From 7e348202c85d1ce1f815c66f5eb69dce18e00e8c Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Wed, 12 Feb 2025 23:04:41 +0000 Subject: [PATCH 01/20] Extract helper from echo server --- .../Handlers/EchoWebSocketHandler.cs | 152 +-------------- .../Handlers/EchoWebSocketHeadersHandler.cs | 49 +---- .../Helpers/WebSocketEchoHelper.cs | 179 ++++++++++++++++++ .../Helpers/WebSocketEchoOptions.cs | 55 ++++++ .../NetCoreServer/NetCoreServer.csproj | 2 + .../tests/AbortTest.Loopback.cs | 2 +- .../LoopbackServer/LoopbackWebSocketServer.cs | 2 +- .../WebSocketHandshakeHelper.cs | 4 +- .../LoopbackServer/WebSocketRequestData.cs | 2 +- .../System.Net.WebSockets.Client.Tests.csproj | 2 + 10 files changed, 258 insertions(+), 191 deletions(-) create mode 100644 src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs create mode 100644 src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs 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..5318730438204f 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,6 +3,7 @@ using System; using System.Net.WebSockets; +using System.Net.WebSockets.Tests; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -12,23 +13,12 @@ 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")) + WebSocketEchoOptions options = WebSocketEchoOptions.Parse(context.Request.QueryString.Value); + if (options.Delay is TimeSpan d) { - await Task.Delay(20000); + await Task.Delay(d); } try @@ -43,146 +33,24 @@ public static async Task InvokeAsync(HttpContext context) } WebSocket socket; - if (!string.IsNullOrEmpty(subProtocol)) + if (!string.IsNullOrEmpty(options.SubProtocol)) { - socket = await context.WebSockets.AcceptWebSocketAsync(subProtocol); + socket = await context.WebSockets.AcceptWebSocketAsync(options.SubProtocol); } else { socket = await context.WebSockets.AcceptWebSocketAsync(); } - await ProcessWebSocketRequest(socket, replyWithPartialMessages, replyWithEnhancedCloseMessage); + await WebSocketEchoHelper.ProcessRequest( + 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..5307d394815f25 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,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.WebSockets; +using System.Net.WebSockets.Tests; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -14,8 +16,6 @@ namespace NetCoreServer { public class EchoWebSocketHeadersHandler { - private const int MaxBufferSize = 1024; - public static async Task InvokeAsync(HttpContext context) { try @@ -30,7 +30,9 @@ public static async Task InvokeAsync(HttpContext context) } WebSocket socket = await context.WebSockets.AcceptWebSocketAsync(); - await ProcessWebSocketRequest(socket, context.Request.Headers); + await WebSocketEchoHelper.ProcessHeadersRequest( + socket, + context.Request.Headers.Select(h => new KeyValuePair(h.Key, h.Value.ToString()))); } catch (Exception) @@ -38,46 +40,5 @@ public static async Task InvokeAsync(HttpContext context) // 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/WebSocketEchoHelper.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs new file mode 100644 index 00000000000000..15bb6b9a76c00e --- /dev/null +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs @@ -0,0 +1,179 @@ +// 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.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.WebSockets.Tests +{ + public static class WebSocketEchoHelper + { + private const int MaxBufferSize = 128 * 1024; + private const int HeadersBufferSize = 1024; + + public static async Task ProcessRequest(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); + } + } + } + + public static async Task ProcessHeadersRequest(WebSocket socket, IEnumerable> headers) + { + var receiveBuffer = new byte[HeadersBufferSize]; + + // 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, 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/WebSocketEchoOptions.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs new file mode 100644 index 00000000000000..5237867716f77c --- /dev/null +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs @@ -0,0 +1,55 @@ +// 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.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace System.Net.WebSockets.Tests +{ + public struct WebSocketEchoOptions + { + public bool ReplyWithPartialMessages { get; set; } + public bool ReplyWithEnhancedCloseMessage { get; set; } + public string SubProtocol { get; set; } + public TimeSpan? Delay { get; set; } + + public static WebSocketEchoOptions Parse(string query) + { + if (query is null or "" or "?") + { + return default; + } + + return new WebSocketEchoOptions + { + ReplyWithPartialMessages = query.Contains("replyWithPartialMessages"), + ReplyWithEnhancedCloseMessage = query.Contains("replyWithEnhancedCloseMessage"), + SubProtocol = ParseSubProtocol(query), + Delay = ParseDelay(query) + }; + } + + private static string ParseSubProtocol(string query) + { + const string subProtocolKey = "subprotocol="; + + var index = query.IndexOf(subProtocolKey); + if (index == -1) + { + return null; + } + + var subProtocol = query.Substring(index + subProtocolKey.Length); + return subProtocol.Contains("&") + ? subProtocol.Substring(0, subProtocol.IndexOf("&")) + : subProtocol; + } + + private static TimeSpan? ParseDelay(string query) + => query.Contains("delay10sec") + ? TimeSpan.FromSeconds(10) + : query.Contains("delay20sec") ? TimeSpan.FromSeconds(20) : null; + } +} 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 d1c331b0aabcdb..f13f797618a71a 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/NetCoreServer.csproj +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/NetCoreServer.csproj @@ -47,6 +47,8 @@ + + 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..95b14197c48aa2 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs @@ -121,7 +121,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) 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..dc1da08450622a 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs @@ -90,7 +90,7 @@ private static async Task RunServerAsync( CancellationToken cancellationToken) { var wsOptions = new WebSocketCreationOptions { IsServer = true }; - var serverWebSocket = WebSocket.CreateFromStream(requestData.WebSocketStream, wsOptions); + var serverWebSocket = WebSocket.CreateFromStream(requestData.TransportStream, wsOptions); await serverWebSocketFunc(serverWebSocket, cancellationToken).ConfigureAwait(false); 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..f70abe07625f64 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketHandshakeHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketHandshakeHelper.cs @@ -43,7 +43,7 @@ public static async Task ProcessHttp11RequestAsync(Loopbac await SendHttp11ServerResponseAsync(connection, secWebSocketKey, cancellationToken).ConfigureAwait(false); } - data.WebSocketStream = connection.Stream; + data.TransportStream = connection.Stream; return data; } @@ -83,7 +83,7 @@ public static async Task ProcessHttp2RequestAsync(Http2Loo 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; } 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..f698d670da3114 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketRequestData.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketRequestData.cs @@ -10,7 +10,7 @@ namespace System.Net.WebSockets.Client.Tests public class WebSocketRequestData { public Dictionary Headers { get; set; } = new Dictionary(); - public Stream? WebSocketStream { 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/System.Net.WebSockets.Client.Tests.csproj b/src/libraries/System.Net.WebSockets.Client/tests/System.Net.WebSockets.Client.Tests.csproj index 98f61386b3eaa8..0841f63376e2ac 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 @@ -46,6 +46,8 @@ + + From 8776dfd0fa916283ed7b969dac91c778cf7e3dbf Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Tue, 18 Feb 2025 21:52:35 +0000 Subject: [PATCH 02/20] Refactor external server tests --- .../System.Net.WebSockets.Client.sln | 519 +++++++++--------- .../tests/AbortTest.cs | 55 +- .../tests/CancelTest.cs | 78 ++- .../tests/CloseTest.cs | 144 +++-- .../tests/ConnectTest.cs | 125 +++-- .../tests/KeepAliveTest.Loopback.cs | 4 - .../LoopbackServer/LoopbackWebSocketServer.cs | 1 + .../tests/SendReceiveTest.cs | 102 ++-- 8 files changed, 575 insertions(+), 453 deletions(-) diff --git a/src/libraries/System.Net.WebSockets.Client/System.Net.WebSockets.Client.sln b/src/libraries/System.Net.WebSockets.Client/System.Net.WebSockets.Client.sln index 11923e8b1b106f..b5a7f9a0294b42 100644 --- a/src/libraries/System.Net.WebSockets.Client/System.Net.WebSockets.Client.sln +++ b/src/libraries/System.Net.WebSockets.Client/System.Net.WebSockets.Client.sln @@ -1,4 +1,8 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35716.79 d17.13 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Private.CoreLib", "..\..\coreclr\System.Private.CoreLib\System.Private.CoreLib.csproj", "{F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{B615DEB1-354C-4357-987A-BBA921E5A712}" @@ -97,16 +101,21 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{A0314AC5-E49 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{58925E96-1A39-4E29-ACB4-4C4DAB81F60A}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "tools\gen", "{78DF8970-30DA-4E8C-8ECE-15935940ACD8}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{78DF8970-30DA-4E8C-8ECE-15935940ACD8}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "tools\src", "{A9E19E1F-375B-4397-B9DB-AA2184FFC68A}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A9E19E1F-375B-4397-B9DB-AA2184FFC68A}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "tools\ref", "{54D53A1C-59C6-47F1-AC04-0C8089382F23}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{54D53A1C-59C6-47F1-AC04-0C8089382F23}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{F1FA4A87-2D90-497E-BAD7-B6D23FDEEBF8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution + Checked|Any CPU = Checked|Any CPU + Checked|arm = Checked|arm + Checked|arm64 = Checked|arm64 + Checked|x64 = Checked|x64 + Checked|x86 = Checked|x86 Debug|Any CPU = Debug|Any CPU Debug|arm = Debug|arm Debug|arm64 = Debug|arm64 @@ -117,13 +126,18 @@ Global Release|arm64 = Release|arm64 Release|x64 = Release|x64 Release|x86 = Release|x86 - Checked|Any CPU = Checked|Any CPU - Checked|arm = Checked|arm - Checked|arm64 = Checked|arm64 - Checked|x64 = Checked|x64 - Checked|x86 = Checked|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|Any CPU.ActiveCfg = Checked|x64 + {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|Any CPU.Build.0 = Checked|x64 + {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|arm.ActiveCfg = Checked|arm + {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|arm.Build.0 = Checked|arm + {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|arm64.ActiveCfg = Checked|arm64 + {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|arm64.Build.0 = Checked|arm64 + {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|x64.ActiveCfg = Checked|x64 + {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|x64.Build.0 = Checked|x64 + {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|x86.ActiveCfg = Checked|x86 + {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|x86.Build.0 = Checked|x86 {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Debug|Any CPU.ActiveCfg = Debug|x64 {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Debug|Any CPU.Build.0 = Debug|x64 {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Debug|arm.ActiveCfg = Debug|arm @@ -144,16 +158,11 @@ Global {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Release|x64.Build.0 = Release|x64 {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Release|x86.ActiveCfg = Release|x86 {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Release|x86.Build.0 = Release|x86 - {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|Any CPU.ActiveCfg = Checked|x64 - {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|Any CPU.Build.0 = Checked|x64 - {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|arm.ActiveCfg = Checked|arm - {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|arm.Build.0 = Checked|arm - {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|arm64.ActiveCfg = Checked|arm64 - {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|arm64.Build.0 = Checked|arm64 - {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|x64.ActiveCfg = Checked|x64 - {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|x64.Build.0 = Checked|x64 - {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|x86.ActiveCfg = Checked|x86 - {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31}.Checked|x86.Build.0 = Checked|x86 + {B615DEB1-354C-4357-987A-BBA921E5A712}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {B615DEB1-354C-4357-987A-BBA921E5A712}.Checked|arm.ActiveCfg = Debug|Any CPU + {B615DEB1-354C-4357-987A-BBA921E5A712}.Checked|arm64.ActiveCfg = Debug|Any CPU + {B615DEB1-354C-4357-987A-BBA921E5A712}.Checked|x64.ActiveCfg = Debug|Any CPU + {B615DEB1-354C-4357-987A-BBA921E5A712}.Checked|x86.ActiveCfg = Debug|Any CPU {B615DEB1-354C-4357-987A-BBA921E5A712}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B615DEB1-354C-4357-987A-BBA921E5A712}.Debug|Any CPU.Build.0 = Debug|Any CPU {B615DEB1-354C-4357-987A-BBA921E5A712}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -170,11 +179,11 @@ Global {B615DEB1-354C-4357-987A-BBA921E5A712}.Release|x64.Build.0 = Release|Any CPU {B615DEB1-354C-4357-987A-BBA921E5A712}.Release|x86.ActiveCfg = Release|Any CPU {B615DEB1-354C-4357-987A-BBA921E5A712}.Release|x86.Build.0 = Release|Any CPU - {B615DEB1-354C-4357-987A-BBA921E5A712}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {B615DEB1-354C-4357-987A-BBA921E5A712}.Checked|arm.ActiveCfg = Debug|Any CPU - {B615DEB1-354C-4357-987A-BBA921E5A712}.Checked|arm64.ActiveCfg = Debug|Any CPU - {B615DEB1-354C-4357-987A-BBA921E5A712}.Checked|x64.ActiveCfg = Debug|Any CPU - {B615DEB1-354C-4357-987A-BBA921E5A712}.Checked|x86.ActiveCfg = Debug|Any CPU + {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Checked|arm.ActiveCfg = Debug|Any CPU + {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Checked|arm64.ActiveCfg = Debug|Any CPU + {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Checked|x64.ActiveCfg = Debug|Any CPU + {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Checked|x86.ActiveCfg = Debug|Any CPU {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Debug|Any CPU.Build.0 = Debug|Any CPU {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -191,11 +200,11 @@ Global {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Release|x64.Build.0 = Release|Any CPU {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Release|x86.ActiveCfg = Release|Any CPU {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Release|x86.Build.0 = Release|Any CPU - {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Checked|arm.ActiveCfg = Debug|Any CPU - {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Checked|arm64.ActiveCfg = Debug|Any CPU - {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Checked|x64.ActiveCfg = Debug|Any CPU - {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D}.Checked|x86.ActiveCfg = Debug|Any CPU + {B3609723-41C3-435B-938F-9AFABB78BFD3}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {B3609723-41C3-435B-938F-9AFABB78BFD3}.Checked|arm.ActiveCfg = Debug|Any CPU + {B3609723-41C3-435B-938F-9AFABB78BFD3}.Checked|arm64.ActiveCfg = Debug|Any CPU + {B3609723-41C3-435B-938F-9AFABB78BFD3}.Checked|x64.ActiveCfg = Debug|Any CPU + {B3609723-41C3-435B-938F-9AFABB78BFD3}.Checked|x86.ActiveCfg = Debug|Any CPU {B3609723-41C3-435B-938F-9AFABB78BFD3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B3609723-41C3-435B-938F-9AFABB78BFD3}.Debug|Any CPU.Build.0 = Debug|Any CPU {B3609723-41C3-435B-938F-9AFABB78BFD3}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -212,11 +221,11 @@ Global {B3609723-41C3-435B-938F-9AFABB78BFD3}.Release|x64.Build.0 = Release|Any CPU {B3609723-41C3-435B-938F-9AFABB78BFD3}.Release|x86.ActiveCfg = Release|Any CPU {B3609723-41C3-435B-938F-9AFABB78BFD3}.Release|x86.Build.0 = Release|Any CPU - {B3609723-41C3-435B-938F-9AFABB78BFD3}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {B3609723-41C3-435B-938F-9AFABB78BFD3}.Checked|arm.ActiveCfg = Debug|Any CPU - {B3609723-41C3-435B-938F-9AFABB78BFD3}.Checked|arm64.ActiveCfg = Debug|Any CPU - {B3609723-41C3-435B-938F-9AFABB78BFD3}.Checked|x64.ActiveCfg = Debug|Any CPU - {B3609723-41C3-435B-938F-9AFABB78BFD3}.Checked|x86.ActiveCfg = Debug|Any CPU + {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Checked|arm.ActiveCfg = Debug|Any CPU + {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Checked|arm64.ActiveCfg = Debug|Any CPU + {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Checked|x64.ActiveCfg = Debug|Any CPU + {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Checked|x86.ActiveCfg = Debug|Any CPU {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Debug|Any CPU.Build.0 = Debug|Any CPU {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -233,11 +242,11 @@ Global {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Release|x64.Build.0 = Release|Any CPU {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Release|x86.ActiveCfg = Release|Any CPU {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Release|x86.Build.0 = Release|Any CPU - {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Checked|arm.ActiveCfg = Debug|Any CPU - {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Checked|arm64.ActiveCfg = Debug|Any CPU - {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Checked|x64.ActiveCfg = Debug|Any CPU - {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4}.Checked|x86.ActiveCfg = Debug|Any CPU + {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Checked|arm.ActiveCfg = Debug|Any CPU + {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Checked|arm64.ActiveCfg = Debug|Any CPU + {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Checked|x64.ActiveCfg = Debug|Any CPU + {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Checked|x86.ActiveCfg = Debug|Any CPU {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Debug|Any CPU.Build.0 = Debug|Any CPU {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -254,11 +263,11 @@ Global {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Release|x64.Build.0 = Release|Any CPU {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Release|x86.ActiveCfg = Release|Any CPU {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Release|x86.Build.0 = Release|Any CPU - {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Checked|arm.ActiveCfg = Debug|Any CPU - {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Checked|arm64.ActiveCfg = Debug|Any CPU - {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Checked|x64.ActiveCfg = Debug|Any CPU - {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01}.Checked|x86.ActiveCfg = Debug|Any CPU + {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Checked|arm.ActiveCfg = Debug|Any CPU + {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Checked|arm64.ActiveCfg = Debug|Any CPU + {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Checked|x64.ActiveCfg = Debug|Any CPU + {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Checked|x86.ActiveCfg = Debug|Any CPU {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Debug|Any CPU.Build.0 = Debug|Any CPU {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -275,11 +284,11 @@ Global {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Release|x64.Build.0 = Release|Any CPU {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Release|x86.ActiveCfg = Release|Any CPU {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Release|x86.Build.0 = Release|Any CPU - {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Checked|arm.ActiveCfg = Debug|Any CPU - {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Checked|arm64.ActiveCfg = Debug|Any CPU - {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Checked|x64.ActiveCfg = Debug|Any CPU - {EC2FAF38-C272-4EA9-9E06-6AE4CD26F07E}.Checked|x86.ActiveCfg = Debug|Any CPU + {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Checked|arm.ActiveCfg = Debug|Any CPU + {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Checked|arm64.ActiveCfg = Debug|Any CPU + {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Checked|x64.ActiveCfg = Debug|Any CPU + {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Checked|x86.ActiveCfg = Debug|Any CPU {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Debug|Any CPU.Build.0 = Debug|Any CPU {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -296,11 +305,11 @@ Global {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Release|x64.Build.0 = Release|Any CPU {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Release|x86.ActiveCfg = Release|Any CPU {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Release|x86.Build.0 = Release|Any CPU - {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Checked|arm.ActiveCfg = Debug|Any CPU - {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Checked|arm64.ActiveCfg = Debug|Any CPU - {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Checked|x64.ActiveCfg = Debug|Any CPU - {F7054DFB-FD2E-469E-9DEB-C3D85F90E1A6}.Checked|x86.ActiveCfg = Debug|Any CPU + {E7329D64-C675-429A-BF0D-24D30ECE762E}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {E7329D64-C675-429A-BF0D-24D30ECE762E}.Checked|arm.ActiveCfg = Debug|Any CPU + {E7329D64-C675-429A-BF0D-24D30ECE762E}.Checked|arm64.ActiveCfg = Debug|Any CPU + {E7329D64-C675-429A-BF0D-24D30ECE762E}.Checked|x64.ActiveCfg = Debug|Any CPU + {E7329D64-C675-429A-BF0D-24D30ECE762E}.Checked|x86.ActiveCfg = Debug|Any CPU {E7329D64-C675-429A-BF0D-24D30ECE762E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E7329D64-C675-429A-BF0D-24D30ECE762E}.Debug|Any CPU.Build.0 = Debug|Any CPU {E7329D64-C675-429A-BF0D-24D30ECE762E}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -317,11 +326,11 @@ Global {E7329D64-C675-429A-BF0D-24D30ECE762E}.Release|x64.Build.0 = Release|Any CPU {E7329D64-C675-429A-BF0D-24D30ECE762E}.Release|x86.ActiveCfg = Release|Any CPU {E7329D64-C675-429A-BF0D-24D30ECE762E}.Release|x86.Build.0 = Release|Any CPU - {E7329D64-C675-429A-BF0D-24D30ECE762E}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {E7329D64-C675-429A-BF0D-24D30ECE762E}.Checked|arm.ActiveCfg = Debug|Any CPU - {E7329D64-C675-429A-BF0D-24D30ECE762E}.Checked|arm64.ActiveCfg = Debug|Any CPU - {E7329D64-C675-429A-BF0D-24D30ECE762E}.Checked|x64.ActiveCfg = Debug|Any CPU - {E7329D64-C675-429A-BF0D-24D30ECE762E}.Checked|x86.ActiveCfg = Debug|Any CPU + {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Checked|arm.ActiveCfg = Debug|Any CPU + {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Checked|arm64.ActiveCfg = Debug|Any CPU + {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Checked|x64.ActiveCfg = Debug|Any CPU + {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Checked|x86.ActiveCfg = Debug|Any CPU {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Debug|Any CPU.Build.0 = Debug|Any CPU {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -338,11 +347,11 @@ Global {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Release|x64.Build.0 = Release|Any CPU {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Release|x86.ActiveCfg = Release|Any CPU {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Release|x86.Build.0 = Release|Any CPU - {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Checked|arm.ActiveCfg = Debug|Any CPU - {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Checked|arm64.ActiveCfg = Debug|Any CPU - {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Checked|x64.ActiveCfg = Debug|Any CPU - {228D8CAA-F7BB-432D-84BC-2D2C45864CC7}.Checked|x86.ActiveCfg = Debug|Any CPU + {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Checked|arm.ActiveCfg = Debug|Any CPU + {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Checked|arm64.ActiveCfg = Debug|Any CPU + {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Checked|x64.ActiveCfg = Debug|Any CPU + {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Checked|x86.ActiveCfg = Debug|Any CPU {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Debug|Any CPU.Build.0 = Debug|Any CPU {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -359,11 +368,11 @@ Global {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Release|x64.Build.0 = Release|Any CPU {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Release|x86.ActiveCfg = Release|Any CPU {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Release|x86.Build.0 = Release|Any CPU - {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Checked|arm.ActiveCfg = Debug|Any CPU - {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Checked|arm64.ActiveCfg = Debug|Any CPU - {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Checked|x64.ActiveCfg = Debug|Any CPU - {408AAFFC-BA73-4BCF-809A-1AEF87E9EA4A}.Checked|x86.ActiveCfg = Debug|Any CPU + {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Checked|arm.ActiveCfg = Debug|Any CPU + {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Checked|arm64.ActiveCfg = Debug|Any CPU + {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Checked|x64.ActiveCfg = Debug|Any CPU + {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Checked|x86.ActiveCfg = Debug|Any CPU {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Debug|Any CPU.Build.0 = Debug|Any CPU {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -380,11 +389,11 @@ Global {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Release|x64.Build.0 = Release|Any CPU {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Release|x86.ActiveCfg = Release|Any CPU {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Release|x86.Build.0 = Release|Any CPU - {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Checked|arm.ActiveCfg = Debug|Any CPU - {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Checked|arm64.ActiveCfg = Debug|Any CPU - {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Checked|x64.ActiveCfg = Debug|Any CPU - {28AD4854-3E18-4863-8328-3F46DF9DA2D1}.Checked|x86.ActiveCfg = Debug|Any CPU + {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Checked|arm.ActiveCfg = Debug|Any CPU + {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Checked|arm64.ActiveCfg = Debug|Any CPU + {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Checked|x64.ActiveCfg = Debug|Any CPU + {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Checked|x86.ActiveCfg = Debug|Any CPU {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Debug|Any CPU.Build.0 = Debug|Any CPU {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -401,11 +410,11 @@ Global {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Release|x64.Build.0 = Release|Any CPU {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Release|x86.ActiveCfg = Release|Any CPU {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Release|x86.Build.0 = Release|Any CPU - {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Checked|arm.ActiveCfg = Debug|Any CPU - {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Checked|arm64.ActiveCfg = Debug|Any CPU - {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Checked|x64.ActiveCfg = Debug|Any CPU - {47D311EC-8502-4DC8-9313-9A0DF9C804F0}.Checked|x86.ActiveCfg = Debug|Any CPU + {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Checked|arm.ActiveCfg = Debug|Any CPU + {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Checked|arm64.ActiveCfg = Debug|Any CPU + {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Checked|x64.ActiveCfg = Debug|Any CPU + {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Checked|x86.ActiveCfg = Debug|Any CPU {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Debug|Any CPU.Build.0 = Debug|Any CPU {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -422,11 +431,11 @@ Global {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Release|x64.Build.0 = Release|Any CPU {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Release|x86.ActiveCfg = Release|Any CPU {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Release|x86.Build.0 = Release|Any CPU - {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Checked|arm.ActiveCfg = Debug|Any CPU - {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Checked|arm64.ActiveCfg = Debug|Any CPU - {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Checked|x64.ActiveCfg = Debug|Any CPU - {780A8ACD-7D55-4A7B-8B6E-638C1A93F622}.Checked|x86.ActiveCfg = Debug|Any CPU + {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Checked|arm.ActiveCfg = Debug|Any CPU + {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Checked|arm64.ActiveCfg = Debug|Any CPU + {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Checked|x64.ActiveCfg = Debug|Any CPU + {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Checked|x86.ActiveCfg = Debug|Any CPU {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Debug|Any CPU.Build.0 = Debug|Any CPU {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -443,11 +452,11 @@ Global {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Release|x64.Build.0 = Release|Any CPU {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Release|x86.ActiveCfg = Release|Any CPU {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Release|x86.Build.0 = Release|Any CPU - {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Checked|arm.ActiveCfg = Debug|Any CPU - {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Checked|arm64.ActiveCfg = Debug|Any CPU - {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Checked|x64.ActiveCfg = Debug|Any CPU - {CC7D7D95-8F05-407A-A8F1-65C8340BE975}.Checked|x86.ActiveCfg = Debug|Any CPU + {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Checked|arm.ActiveCfg = Debug|Any CPU + {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Checked|arm64.ActiveCfg = Debug|Any CPU + {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Checked|x64.ActiveCfg = Debug|Any CPU + {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Checked|x86.ActiveCfg = Debug|Any CPU {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Debug|Any CPU.Build.0 = Debug|Any CPU {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -464,11 +473,11 @@ Global {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Release|x64.Build.0 = Release|Any CPU {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Release|x86.ActiveCfg = Release|Any CPU {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Release|x86.Build.0 = Release|Any CPU - {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Checked|arm.ActiveCfg = Debug|Any CPU - {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Checked|arm64.ActiveCfg = Debug|Any CPU - {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Checked|x64.ActiveCfg = Debug|Any CPU - {29A6C0D2-D7F1-4947-A77B-DBE0870F034C}.Checked|x86.ActiveCfg = Debug|Any CPU + {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Checked|arm.ActiveCfg = Debug|Any CPU + {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Checked|arm64.ActiveCfg = Debug|Any CPU + {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Checked|x64.ActiveCfg = Debug|Any CPU + {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Checked|x86.ActiveCfg = Debug|Any CPU {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Debug|Any CPU.Build.0 = Debug|Any CPU {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -485,11 +494,11 @@ Global {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Release|x64.Build.0 = Release|Any CPU {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Release|x86.ActiveCfg = Release|Any CPU {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Release|x86.Build.0 = Release|Any CPU - {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Checked|arm.ActiveCfg = Debug|Any CPU - {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Checked|arm64.ActiveCfg = Debug|Any CPU - {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Checked|x64.ActiveCfg = Debug|Any CPU - {BEA5BC2C-12D1-4D01-8D2C-5029578BD066}.Checked|x86.ActiveCfg = Debug|Any CPU + {0CD4C24D-7746-46F0-8D47-A396882B5468}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {0CD4C24D-7746-46F0-8D47-A396882B5468}.Checked|arm.ActiveCfg = Debug|Any CPU + {0CD4C24D-7746-46F0-8D47-A396882B5468}.Checked|arm64.ActiveCfg = Debug|Any CPU + {0CD4C24D-7746-46F0-8D47-A396882B5468}.Checked|x64.ActiveCfg = Debug|Any CPU + {0CD4C24D-7746-46F0-8D47-A396882B5468}.Checked|x86.ActiveCfg = Debug|Any CPU {0CD4C24D-7746-46F0-8D47-A396882B5468}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0CD4C24D-7746-46F0-8D47-A396882B5468}.Debug|Any CPU.Build.0 = Debug|Any CPU {0CD4C24D-7746-46F0-8D47-A396882B5468}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -506,11 +515,11 @@ Global {0CD4C24D-7746-46F0-8D47-A396882B5468}.Release|x64.Build.0 = Release|Any CPU {0CD4C24D-7746-46F0-8D47-A396882B5468}.Release|x86.ActiveCfg = Release|Any CPU {0CD4C24D-7746-46F0-8D47-A396882B5468}.Release|x86.Build.0 = Release|Any CPU - {0CD4C24D-7746-46F0-8D47-A396882B5468}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {0CD4C24D-7746-46F0-8D47-A396882B5468}.Checked|arm.ActiveCfg = Debug|Any CPU - {0CD4C24D-7746-46F0-8D47-A396882B5468}.Checked|arm64.ActiveCfg = Debug|Any CPU - {0CD4C24D-7746-46F0-8D47-A396882B5468}.Checked|x64.ActiveCfg = Debug|Any CPU - {0CD4C24D-7746-46F0-8D47-A396882B5468}.Checked|x86.ActiveCfg = Debug|Any CPU + {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Checked|arm.ActiveCfg = Debug|Any CPU + {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Checked|arm64.ActiveCfg = Debug|Any CPU + {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Checked|x64.ActiveCfg = Debug|Any CPU + {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Checked|x86.ActiveCfg = Debug|Any CPU {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Debug|Any CPU.Build.0 = Debug|Any CPU {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -527,11 +536,11 @@ Global {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Release|x64.Build.0 = Release|Any CPU {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Release|x86.ActiveCfg = Release|Any CPU {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Release|x86.Build.0 = Release|Any CPU - {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Checked|arm.ActiveCfg = Debug|Any CPU - {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Checked|arm64.ActiveCfg = Debug|Any CPU - {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Checked|x64.ActiveCfg = Debug|Any CPU - {90E8DA45-66F3-491E-B408-82AB85EEAB76}.Checked|x86.ActiveCfg = Debug|Any CPU + {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Checked|arm.ActiveCfg = Debug|Any CPU + {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Checked|arm64.ActiveCfg = Debug|Any CPU + {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Checked|x64.ActiveCfg = Debug|Any CPU + {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Checked|x86.ActiveCfg = Debug|Any CPU {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Debug|Any CPU.Build.0 = Debug|Any CPU {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -548,11 +557,11 @@ Global {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Release|x64.Build.0 = Release|Any CPU {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Release|x86.ActiveCfg = Release|Any CPU {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Release|x86.Build.0 = Release|Any CPU - {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Checked|arm.ActiveCfg = Debug|Any CPU - {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Checked|arm64.ActiveCfg = Debug|Any CPU - {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Checked|x64.ActiveCfg = Debug|Any CPU - {CA20532A-33B3-4DC0-92D2-EA6D7987D59F}.Checked|x86.ActiveCfg = Debug|Any CPU + {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Checked|arm.ActiveCfg = Debug|Any CPU + {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Checked|arm64.ActiveCfg = Debug|Any CPU + {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Checked|x64.ActiveCfg = Debug|Any CPU + {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Checked|x86.ActiveCfg = Debug|Any CPU {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Debug|Any CPU.Build.0 = Debug|Any CPU {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -569,11 +578,11 @@ Global {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Release|x64.Build.0 = Release|Any CPU {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Release|x86.ActiveCfg = Release|Any CPU {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Release|x86.Build.0 = Release|Any CPU - {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Checked|arm.ActiveCfg = Debug|Any CPU - {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Checked|arm64.ActiveCfg = Debug|Any CPU - {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Checked|x64.ActiveCfg = Debug|Any CPU - {96065341-8032-40B4-A1B5-C9DC83DFEF4E}.Checked|x86.ActiveCfg = Debug|Any CPU + {CF0716D1-9916-4A97-939C-2FE477193A8F}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {CF0716D1-9916-4A97-939C-2FE477193A8F}.Checked|arm.ActiveCfg = Debug|Any CPU + {CF0716D1-9916-4A97-939C-2FE477193A8F}.Checked|arm64.ActiveCfg = Debug|Any CPU + {CF0716D1-9916-4A97-939C-2FE477193A8F}.Checked|x64.ActiveCfg = Debug|Any CPU + {CF0716D1-9916-4A97-939C-2FE477193A8F}.Checked|x86.ActiveCfg = Debug|Any CPU {CF0716D1-9916-4A97-939C-2FE477193A8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {CF0716D1-9916-4A97-939C-2FE477193A8F}.Debug|Any CPU.Build.0 = Debug|Any CPU {CF0716D1-9916-4A97-939C-2FE477193A8F}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -590,11 +599,11 @@ Global {CF0716D1-9916-4A97-939C-2FE477193A8F}.Release|x64.Build.0 = Release|Any CPU {CF0716D1-9916-4A97-939C-2FE477193A8F}.Release|x86.ActiveCfg = Release|Any CPU {CF0716D1-9916-4A97-939C-2FE477193A8F}.Release|x86.Build.0 = Release|Any CPU - {CF0716D1-9916-4A97-939C-2FE477193A8F}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {CF0716D1-9916-4A97-939C-2FE477193A8F}.Checked|arm.ActiveCfg = Debug|Any CPU - {CF0716D1-9916-4A97-939C-2FE477193A8F}.Checked|arm64.ActiveCfg = Debug|Any CPU - {CF0716D1-9916-4A97-939C-2FE477193A8F}.Checked|x64.ActiveCfg = Debug|Any CPU - {CF0716D1-9916-4A97-939C-2FE477193A8F}.Checked|x86.ActiveCfg = Debug|Any CPU + {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Checked|arm.ActiveCfg = Debug|Any CPU + {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Checked|arm64.ActiveCfg = Debug|Any CPU + {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Checked|x64.ActiveCfg = Debug|Any CPU + {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Checked|x86.ActiveCfg = Debug|Any CPU {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Debug|Any CPU.Build.0 = Debug|Any CPU {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -611,11 +620,11 @@ Global {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Release|x64.Build.0 = Release|Any CPU {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Release|x86.ActiveCfg = Release|Any CPU {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Release|x86.Build.0 = Release|Any CPU - {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Checked|arm.ActiveCfg = Debug|Any CPU - {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Checked|arm64.ActiveCfg = Debug|Any CPU - {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Checked|x64.ActiveCfg = Debug|Any CPU - {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64}.Checked|x86.ActiveCfg = Debug|Any CPU + {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Checked|arm.ActiveCfg = Debug|Any CPU + {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Checked|arm64.ActiveCfg = Debug|Any CPU + {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Checked|x64.ActiveCfg = Debug|Any CPU + {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Checked|x86.ActiveCfg = Debug|Any CPU {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Debug|Any CPU.Build.0 = Debug|Any CPU {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -632,11 +641,11 @@ Global {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Release|x64.Build.0 = Release|Any CPU {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Release|x86.ActiveCfg = Release|Any CPU {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Release|x86.Build.0 = Release|Any CPU - {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Checked|arm.ActiveCfg = Debug|Any CPU - {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Checked|arm64.ActiveCfg = Debug|Any CPU - {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Checked|x64.ActiveCfg = Debug|Any CPU - {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5}.Checked|x86.ActiveCfg = Debug|Any CPU + {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Checked|arm.ActiveCfg = Debug|Any CPU + {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Checked|arm64.ActiveCfg = Debug|Any CPU + {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Checked|x64.ActiveCfg = Debug|Any CPU + {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Checked|x86.ActiveCfg = Debug|Any CPU {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Debug|Any CPU.Build.0 = Debug|Any CPU {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -653,11 +662,11 @@ Global {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Release|x64.Build.0 = Release|Any CPU {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Release|x86.ActiveCfg = Release|Any CPU {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Release|x86.Build.0 = Release|Any CPU - {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Checked|arm.ActiveCfg = Debug|Any CPU - {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Checked|arm64.ActiveCfg = Debug|Any CPU - {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Checked|x64.ActiveCfg = Debug|Any CPU - {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9}.Checked|x86.ActiveCfg = Debug|Any CPU + {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Checked|arm.ActiveCfg = Debug|Any CPU + {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Checked|arm64.ActiveCfg = Debug|Any CPU + {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Checked|x64.ActiveCfg = Debug|Any CPU + {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Checked|x86.ActiveCfg = Debug|Any CPU {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Debug|Any CPU.Build.0 = Debug|Any CPU {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -674,11 +683,11 @@ Global {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Release|x64.Build.0 = Release|Any CPU {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Release|x86.ActiveCfg = Release|Any CPU {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Release|x86.Build.0 = Release|Any CPU - {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Checked|arm.ActiveCfg = Debug|Any CPU - {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Checked|arm64.ActiveCfg = Debug|Any CPU - {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Checked|x64.ActiveCfg = Debug|Any CPU - {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E}.Checked|x86.ActiveCfg = Debug|Any CPU + {4C06C0C8-67D8-4329-B083-6906261C8838}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {4C06C0C8-67D8-4329-B083-6906261C8838}.Checked|arm.ActiveCfg = Debug|Any CPU + {4C06C0C8-67D8-4329-B083-6906261C8838}.Checked|arm64.ActiveCfg = Debug|Any CPU + {4C06C0C8-67D8-4329-B083-6906261C8838}.Checked|x64.ActiveCfg = Debug|Any CPU + {4C06C0C8-67D8-4329-B083-6906261C8838}.Checked|x86.ActiveCfg = Debug|Any CPU {4C06C0C8-67D8-4329-B083-6906261C8838}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4C06C0C8-67D8-4329-B083-6906261C8838}.Debug|Any CPU.Build.0 = Debug|Any CPU {4C06C0C8-67D8-4329-B083-6906261C8838}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -695,11 +704,11 @@ Global {4C06C0C8-67D8-4329-B083-6906261C8838}.Release|x64.Build.0 = Release|Any CPU {4C06C0C8-67D8-4329-B083-6906261C8838}.Release|x86.ActiveCfg = Release|Any CPU {4C06C0C8-67D8-4329-B083-6906261C8838}.Release|x86.Build.0 = Release|Any CPU - {4C06C0C8-67D8-4329-B083-6906261C8838}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {4C06C0C8-67D8-4329-B083-6906261C8838}.Checked|arm.ActiveCfg = Debug|Any CPU - {4C06C0C8-67D8-4329-B083-6906261C8838}.Checked|arm64.ActiveCfg = Debug|Any CPU - {4C06C0C8-67D8-4329-B083-6906261C8838}.Checked|x64.ActiveCfg = Debug|Any CPU - {4C06C0C8-67D8-4329-B083-6906261C8838}.Checked|x86.ActiveCfg = Debug|Any CPU + {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Checked|arm.ActiveCfg = Debug|Any CPU + {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Checked|arm64.ActiveCfg = Debug|Any CPU + {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Checked|x64.ActiveCfg = Debug|Any CPU + {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Checked|x86.ActiveCfg = Debug|Any CPU {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Debug|Any CPU.Build.0 = Debug|Any CPU {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -716,11 +725,11 @@ Global {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Release|x64.Build.0 = Release|Any CPU {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Release|x86.ActiveCfg = Release|Any CPU {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Release|x86.Build.0 = Release|Any CPU - {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Checked|arm.ActiveCfg = Debug|Any CPU - {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Checked|arm64.ActiveCfg = Debug|Any CPU - {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Checked|x64.ActiveCfg = Debug|Any CPU - {61BC8746-42CB-452E-AF4B-A2555C1DAB2F}.Checked|x86.ActiveCfg = Debug|Any CPU + {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Checked|arm.ActiveCfg = Debug|Any CPU + {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Checked|arm64.ActiveCfg = Debug|Any CPU + {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Checked|x64.ActiveCfg = Debug|Any CPU + {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Checked|x86.ActiveCfg = Debug|Any CPU {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Debug|Any CPU.Build.0 = Debug|Any CPU {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -737,11 +746,11 @@ Global {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Release|x64.Build.0 = Release|Any CPU {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Release|x86.ActiveCfg = Release|Any CPU {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Release|x86.Build.0 = Release|Any CPU - {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Checked|arm.ActiveCfg = Debug|Any CPU - {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Checked|arm64.ActiveCfg = Debug|Any CPU - {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Checked|x64.ActiveCfg = Debug|Any CPU - {4F8ABE5C-1400-481D-96E5-5D922713BB72}.Checked|x86.ActiveCfg = Debug|Any CPU + {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Checked|arm.ActiveCfg = Debug|Any CPU + {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Checked|arm64.ActiveCfg = Debug|Any CPU + {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Checked|x64.ActiveCfg = Debug|Any CPU + {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Checked|x86.ActiveCfg = Debug|Any CPU {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -758,11 +767,11 @@ Global {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Release|x64.Build.0 = Release|Any CPU {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Release|x86.ActiveCfg = Release|Any CPU {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Release|x86.Build.0 = Release|Any CPU - {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Checked|arm.ActiveCfg = Debug|Any CPU - {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Checked|arm64.ActiveCfg = Debug|Any CPU - {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Checked|x64.ActiveCfg = Debug|Any CPU - {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE}.Checked|x86.ActiveCfg = Debug|Any CPU + {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Checked|arm.ActiveCfg = Debug|Any CPU + {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Checked|arm64.ActiveCfg = Debug|Any CPU + {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Checked|x64.ActiveCfg = Debug|Any CPU + {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Checked|x86.ActiveCfg = Debug|Any CPU {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Debug|Any CPU.Build.0 = Debug|Any CPU {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -779,11 +788,11 @@ Global {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Release|x64.Build.0 = Release|Any CPU {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Release|x86.ActiveCfg = Release|Any CPU {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Release|x86.Build.0 = Release|Any CPU - {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Checked|arm.ActiveCfg = Debug|Any CPU - {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Checked|arm64.ActiveCfg = Debug|Any CPU - {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Checked|x64.ActiveCfg = Debug|Any CPU - {6659E37B-9C84-4815-9CAB-19F367D3D66D}.Checked|x86.ActiveCfg = Debug|Any CPU + {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Checked|arm.ActiveCfg = Debug|Any CPU + {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Checked|arm64.ActiveCfg = Debug|Any CPU + {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Checked|x64.ActiveCfg = Debug|Any CPU + {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Checked|x86.ActiveCfg = Debug|Any CPU {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Debug|Any CPU.Build.0 = Debug|Any CPU {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -800,11 +809,11 @@ Global {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Release|x64.Build.0 = Release|Any CPU {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Release|x86.ActiveCfg = Release|Any CPU {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Release|x86.Build.0 = Release|Any CPU - {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Checked|arm.ActiveCfg = Debug|Any CPU - {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Checked|arm64.ActiveCfg = Debug|Any CPU - {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Checked|x64.ActiveCfg = Debug|Any CPU - {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12}.Checked|x86.ActiveCfg = Debug|Any CPU + {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Checked|arm.ActiveCfg = Debug|Any CPU + {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Checked|arm64.ActiveCfg = Debug|Any CPU + {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Checked|x64.ActiveCfg = Debug|Any CPU + {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Checked|x86.ActiveCfg = Debug|Any CPU {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Debug|Any CPU.Build.0 = Debug|Any CPU {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -821,11 +830,11 @@ Global {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Release|x64.Build.0 = Release|Any CPU {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Release|x86.ActiveCfg = Release|Any CPU {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Release|x86.Build.0 = Release|Any CPU - {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Checked|arm.ActiveCfg = Debug|Any CPU - {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Checked|arm64.ActiveCfg = Debug|Any CPU - {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Checked|x64.ActiveCfg = Debug|Any CPU - {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E}.Checked|x86.ActiveCfg = Debug|Any CPU + {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Checked|arm.ActiveCfg = Debug|Any CPU + {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Checked|arm64.ActiveCfg = Debug|Any CPU + {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Checked|x64.ActiveCfg = Debug|Any CPU + {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Checked|x86.ActiveCfg = Debug|Any CPU {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Debug|Any CPU.Build.0 = Debug|Any CPU {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -842,11 +851,11 @@ Global {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Release|x64.Build.0 = Release|Any CPU {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Release|x86.ActiveCfg = Release|Any CPU {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Release|x86.Build.0 = Release|Any CPU - {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Checked|arm.ActiveCfg = Debug|Any CPU - {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Checked|arm64.ActiveCfg = Debug|Any CPU - {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Checked|x64.ActiveCfg = Debug|Any CPU - {49C959C3-D2C1-4A66-B29E-FE8E393BC274}.Checked|x86.ActiveCfg = Debug|Any CPU + {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Checked|arm.ActiveCfg = Debug|Any CPU + {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Checked|arm64.ActiveCfg = Debug|Any CPU + {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Checked|x64.ActiveCfg = Debug|Any CPU + {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Checked|x86.ActiveCfg = Debug|Any CPU {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Debug|Any CPU.Build.0 = Debug|Any CPU {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -863,11 +872,11 @@ Global {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Release|x64.Build.0 = Release|Any CPU {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Release|x86.ActiveCfg = Release|Any CPU {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Release|x86.Build.0 = Release|Any CPU - {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Checked|arm.ActiveCfg = Debug|Any CPU - {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Checked|arm64.ActiveCfg = Debug|Any CPU - {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Checked|x64.ActiveCfg = Debug|Any CPU - {38DF9841-78CF-4E40-9DB7-9926673B83FE}.Checked|x86.ActiveCfg = Debug|Any CPU + {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Checked|arm.ActiveCfg = Debug|Any CPU + {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Checked|arm64.ActiveCfg = Debug|Any CPU + {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Checked|x64.ActiveCfg = Debug|Any CPU + {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Checked|x86.ActiveCfg = Debug|Any CPU {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Debug|Any CPU.Build.0 = Debug|Any CPU {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -884,11 +893,11 @@ Global {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Release|x64.Build.0 = Release|Any CPU {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Release|x86.ActiveCfg = Release|Any CPU {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Release|x86.Build.0 = Release|Any CPU - {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Checked|arm.ActiveCfg = Debug|Any CPU - {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Checked|arm64.ActiveCfg = Debug|Any CPU - {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Checked|x64.ActiveCfg = Debug|Any CPU - {620776C7-6DE5-4398-8C93-D30FF68C3C11}.Checked|x86.ActiveCfg = Debug|Any CPU + {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Checked|arm.ActiveCfg = Debug|Any CPU + {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Checked|arm64.ActiveCfg = Debug|Any CPU + {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Checked|x64.ActiveCfg = Debug|Any CPU + {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Checked|x86.ActiveCfg = Debug|Any CPU {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Debug|Any CPU.Build.0 = Debug|Any CPU {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -905,11 +914,11 @@ Global {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Release|x64.Build.0 = Release|Any CPU {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Release|x86.ActiveCfg = Release|Any CPU {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Release|x86.Build.0 = Release|Any CPU - {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Checked|arm.ActiveCfg = Debug|Any CPU - {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Checked|arm64.ActiveCfg = Debug|Any CPU - {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Checked|x64.ActiveCfg = Debug|Any CPU - {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24}.Checked|x86.ActiveCfg = Debug|Any CPU + {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Checked|arm.ActiveCfg = Debug|Any CPU + {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Checked|arm64.ActiveCfg = Debug|Any CPU + {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Checked|x64.ActiveCfg = Debug|Any CPU + {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Checked|x86.ActiveCfg = Debug|Any CPU {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Debug|Any CPU.Build.0 = Debug|Any CPU {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -926,11 +935,11 @@ Global {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Release|x64.Build.0 = Release|Any CPU {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Release|x86.ActiveCfg = Release|Any CPU {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Release|x86.Build.0 = Release|Any CPU - {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Checked|arm.ActiveCfg = Debug|Any CPU - {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Checked|arm64.ActiveCfg = Debug|Any CPU - {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Checked|x64.ActiveCfg = Debug|Any CPU - {74CFCC98-93DD-408F-8977-EF9EE158F1D1}.Checked|x86.ActiveCfg = Debug|Any CPU + {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Checked|arm.ActiveCfg = Debug|Any CPU + {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Checked|arm64.ActiveCfg = Debug|Any CPU + {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Checked|x64.ActiveCfg = Debug|Any CPU + {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Checked|x86.ActiveCfg = Debug|Any CPU {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Debug|Any CPU.Build.0 = Debug|Any CPU {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -947,11 +956,11 @@ Global {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Release|x64.Build.0 = Release|Any CPU {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Release|x86.ActiveCfg = Release|Any CPU {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Release|x86.Build.0 = Release|Any CPU - {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Checked|arm.ActiveCfg = Debug|Any CPU - {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Checked|arm64.ActiveCfg = Debug|Any CPU - {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Checked|x64.ActiveCfg = Debug|Any CPU - {D6014E91-2DAB-4459-B94A-F1B85EF1D422}.Checked|x86.ActiveCfg = Debug|Any CPU + {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Checked|arm.ActiveCfg = Debug|Any CPU + {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Checked|arm64.ActiveCfg = Debug|Any CPU + {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Checked|x64.ActiveCfg = Debug|Any CPU + {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Checked|x86.ActiveCfg = Debug|Any CPU {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Debug|Any CPU.Build.0 = Debug|Any CPU {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -968,11 +977,11 @@ Global {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Release|x64.Build.0 = Release|Any CPU {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Release|x86.ActiveCfg = Release|Any CPU {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Release|x86.Build.0 = Release|Any CPU - {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Checked|arm.ActiveCfg = Debug|Any CPU - {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Checked|arm64.ActiveCfg = Debug|Any CPU - {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Checked|x64.ActiveCfg = Debug|Any CPU - {94CE2C38-AB0C-4E53-8B52-4376428E4E7A}.Checked|x86.ActiveCfg = Debug|Any CPU + {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Checked|arm.ActiveCfg = Debug|Any CPU + {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Checked|arm64.ActiveCfg = Debug|Any CPU + {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Checked|x64.ActiveCfg = Debug|Any CPU + {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Checked|x86.ActiveCfg = Debug|Any CPU {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Debug|Any CPU.Build.0 = Debug|Any CPU {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -989,11 +998,11 @@ Global {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Release|x64.Build.0 = Release|Any CPU {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Release|x86.ActiveCfg = Release|Any CPU {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Release|x86.Build.0 = Release|Any CPU - {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Checked|arm.ActiveCfg = Debug|Any CPU - {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Checked|arm64.ActiveCfg = Debug|Any CPU - {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Checked|x64.ActiveCfg = Debug|Any CPU - {619B12CC-42E2-472F-9934-BBCEE34BDAD2}.Checked|x86.ActiveCfg = Debug|Any CPU + {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Checked|arm.ActiveCfg = Debug|Any CPU + {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Checked|arm64.ActiveCfg = Debug|Any CPU + {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Checked|x64.ActiveCfg = Debug|Any CPU + {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Checked|x86.ActiveCfg = Debug|Any CPU {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Debug|Any CPU.Build.0 = Debug|Any CPU {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -1010,11 +1019,11 @@ Global {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Release|x64.Build.0 = Release|Any CPU {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Release|x86.ActiveCfg = Release|Any CPU {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Release|x86.Build.0 = Release|Any CPU - {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Checked|arm.ActiveCfg = Debug|Any CPU - {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Checked|arm64.ActiveCfg = Debug|Any CPU - {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Checked|x64.ActiveCfg = Debug|Any CPU - {E579B589-D9C9-4A77-BC2C-A8EAD293A603}.Checked|x86.ActiveCfg = Debug|Any CPU + {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Checked|arm.ActiveCfg = Debug|Any CPU + {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Checked|arm64.ActiveCfg = Debug|Any CPU + {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Checked|x64.ActiveCfg = Debug|Any CPU + {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Checked|x86.ActiveCfg = Debug|Any CPU {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Debug|Any CPU.Build.0 = Debug|Any CPU {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -1031,11 +1040,11 @@ Global {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Release|x64.Build.0 = Release|Any CPU {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Release|x86.ActiveCfg = Release|Any CPU {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Release|x86.Build.0 = Release|Any CPU - {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Checked|arm.ActiveCfg = Debug|Any CPU - {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Checked|arm64.ActiveCfg = Debug|Any CPU - {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Checked|x64.ActiveCfg = Debug|Any CPU - {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC}.Checked|x86.ActiveCfg = Debug|Any CPU + {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Checked|arm.ActiveCfg = Debug|Any CPU + {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Checked|arm64.ActiveCfg = Debug|Any CPU + {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Checked|x64.ActiveCfg = Debug|Any CPU + {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Checked|x86.ActiveCfg = Debug|Any CPU {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Debug|Any CPU.Build.0 = Debug|Any CPU {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -1052,11 +1061,11 @@ Global {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Release|x64.Build.0 = Release|Any CPU {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Release|x86.ActiveCfg = Release|Any CPU {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Release|x86.Build.0 = Release|Any CPU - {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Checked|arm.ActiveCfg = Debug|Any CPU - {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Checked|arm64.ActiveCfg = Debug|Any CPU - {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Checked|x64.ActiveCfg = Debug|Any CPU - {DFB3F30A-21B6-481A-BE52-E7FABDB75794}.Checked|x86.ActiveCfg = Debug|Any CPU + {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Checked|Any CPU.ActiveCfg = Debug|Any CPU + {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Checked|arm.ActiveCfg = Debug|Any CPU + {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Checked|arm64.ActiveCfg = Debug|Any CPU + {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Checked|x64.ActiveCfg = Debug|Any CPU + {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Checked|x86.ActiveCfg = Debug|Any CPU {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Debug|Any CPU.Build.0 = Debug|Any CPU {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Debug|arm.ActiveCfg = Debug|Any CPU @@ -1073,27 +1082,14 @@ Global {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Release|x64.Build.0 = Release|Any CPU {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Release|x86.ActiveCfg = Release|Any CPU {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Release|x86.Build.0 = Release|Any CPU - {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Checked|Any CPU.ActiveCfg = Debug|Any CPU - {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Checked|arm.ActiveCfg = Debug|Any CPU - {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Checked|arm64.ActiveCfg = Debug|Any CPU - {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Checked|x64.ActiveCfg = Debug|Any CPU - {D55C29A5-A426-4D63-B188-670E30BDA3AE}.Checked|x86.ActiveCfg = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {F454FCEE-B5BE-406D-A64F-FBFBCAB3EE31} = {6F9A42A0-A04B-4CD0-B8C9-9A728274C851} - {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D} = {6F9A42A0-A04B-4CD0-B8C9-9A728274C851} - {0CD4C24D-7746-46F0-8D47-A396882B5468} = {6F9A42A0-A04B-4CD0-B8C9-9A728274C851} - {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9} = {6F9A42A0-A04B-4CD0-B8C9-9A728274C851} - {61BC8746-42CB-452E-AF4B-A2555C1DAB2F} = {6F9A42A0-A04B-4CD0-B8C9-9A728274C851} - {38DF9841-78CF-4E40-9DB7-9926673B83FE} = {6F9A42A0-A04B-4CD0-B8C9-9A728274C851} - {74CFCC98-93DD-408F-8977-EF9EE158F1D1} = {6F9A42A0-A04B-4CD0-B8C9-9A728274C851} - {94CE2C38-AB0C-4E53-8B52-4376428E4E7A} = {6F9A42A0-A04B-4CD0-B8C9-9A728274C851} {B615DEB1-354C-4357-987A-BBA921E5A712} = {BEE2F256-0489-4809-AB20-27ADB2D0E10C} - {90E8DA45-66F3-491E-B408-82AB85EEAB76} = {BEE2F256-0489-4809-AB20-27ADB2D0E10C} - {CA20532A-33B3-4DC0-92D2-EA6D7987D59F} = {BEE2F256-0489-4809-AB20-27ADB2D0E10C} + {09C4ABB6-301C-44E5-9972-6BAE4B4A2A9D} = {6F9A42A0-A04B-4CD0-B8C9-9A728274C851} {B3609723-41C3-435B-938F-9AFABB78BFD3} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} {9FE423E7-F6BE-4DB2-A3D0-E5DDAE1DB6B4} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} {68BAD4CC-08BA-4158-8CEA-5C7F648B5C01} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} @@ -1108,31 +1104,44 @@ Global {CC7D7D95-8F05-407A-A8F1-65C8340BE975} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} {29A6C0D2-D7F1-4947-A77B-DBE0870F034C} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} {BEA5BC2C-12D1-4D01-8D2C-5029578BD066} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} + {0CD4C24D-7746-46F0-8D47-A396882B5468} = {6F9A42A0-A04B-4CD0-B8C9-9A728274C851} + {90E8DA45-66F3-491E-B408-82AB85EEAB76} = {BEE2F256-0489-4809-AB20-27ADB2D0E10C} + {CA20532A-33B3-4DC0-92D2-EA6D7987D59F} = {BEE2F256-0489-4809-AB20-27ADB2D0E10C} {96065341-8032-40B4-A1B5-C9DC83DFEF4E} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} {CF0716D1-9916-4A97-939C-2FE477193A8F} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} + {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64} = {58925E96-1A39-4E29-ACB4-4C4DAB81F60A} {D9D68B05-1764-4448-8C3C-BEEB6FEC26A5} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} + {0EE7C4E9-CEA2-44C7-B1AD-590A026A70F9} = {6F9A42A0-A04B-4CD0-B8C9-9A728274C851} + {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E} = {58925E96-1A39-4E29-ACB4-4C4DAB81F60A} {4C06C0C8-67D8-4329-B083-6906261C8838} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} + {61BC8746-42CB-452E-AF4B-A2555C1DAB2F} = {6F9A42A0-A04B-4CD0-B8C9-9A728274C851} + {4F8ABE5C-1400-481D-96E5-5D922713BB72} = {58925E96-1A39-4E29-ACB4-4C4DAB81F60A} + {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE} = {58925E96-1A39-4E29-ACB4-4C4DAB81F60A} + {6659E37B-9C84-4815-9CAB-19F367D3D66D} = {58925E96-1A39-4E29-ACB4-4C4DAB81F60A} {2BD4ACB1-47A4-4AA5-9624-6DC939B4EB12} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} {A4995F4A-D42F-47AD-BFF1-A46BC1B4574E} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} {49C959C3-D2C1-4A66-B29E-FE8E393BC274} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} + {38DF9841-78CF-4E40-9DB7-9926673B83FE} = {6F9A42A0-A04B-4CD0-B8C9-9A728274C851} {620776C7-6DE5-4398-8C93-D30FF68C3C11} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} {5DCE41F7-CA1D-49F2-ABB0-41BF60052B24} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} + {74CFCC98-93DD-408F-8977-EF9EE158F1D1} = {6F9A42A0-A04B-4CD0-B8C9-9A728274C851} {D6014E91-2DAB-4459-B94A-F1B85EF1D422} = {A0314AC5-E490-4A6A-B946-8B9A21A2FA05} - {1EB7F21B-0ED8-426F-B4A0-150BA26B4E64} = {58925E96-1A39-4E29-ACB4-4C4DAB81F60A} - {5C036B23-44E0-4F0D-9CE3-DE29E66DFE7E} = {58925E96-1A39-4E29-ACB4-4C4DAB81F60A} - {4F8ABE5C-1400-481D-96E5-5D922713BB72} = {58925E96-1A39-4E29-ACB4-4C4DAB81F60A} - {7A0C1B6C-0439-4A1F-95CC-67F04A9657FE} = {58925E96-1A39-4E29-ACB4-4C4DAB81F60A} - {6659E37B-9C84-4815-9CAB-19F367D3D66D} = {58925E96-1A39-4E29-ACB4-4C4DAB81F60A} + {94CE2C38-AB0C-4E53-8B52-4376428E4E7A} = {6F9A42A0-A04B-4CD0-B8C9-9A728274C851} {619B12CC-42E2-472F-9934-BBCEE34BDAD2} = {78DF8970-30DA-4E8C-8ECE-15935940ACD8} {E579B589-D9C9-4A77-BC2C-A8EAD293A603} = {78DF8970-30DA-4E8C-8ECE-15935940ACD8} - {78DF8970-30DA-4E8C-8ECE-15935940ACD8} = {F1FA4A87-2D90-497E-BAD7-B6D23FDEEBF8} {C0C9A9C0-51E4-4627-9221-E9411E2C0FAC} = {A9E19E1F-375B-4397-B9DB-AA2184FFC68A} {DFB3F30A-21B6-481A-BE52-E7FABDB75794} = {A9E19E1F-375B-4397-B9DB-AA2184FFC68A} - {A9E19E1F-375B-4397-B9DB-AA2184FFC68A} = {F1FA4A87-2D90-497E-BAD7-B6D23FDEEBF8} {D55C29A5-A426-4D63-B188-670E30BDA3AE} = {54D53A1C-59C6-47F1-AC04-0C8089382F23} + {78DF8970-30DA-4E8C-8ECE-15935940ACD8} = {F1FA4A87-2D90-497E-BAD7-B6D23FDEEBF8} + {A9E19E1F-375B-4397-B9DB-AA2184FFC68A} = {F1FA4A87-2D90-497E-BAD7-B6D23FDEEBF8} {54D53A1C-59C6-47F1-AC04-0C8089382F23} = {F1FA4A87-2D90-497E-BAD7-B6D23FDEEBF8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D91D7DC5-24CC-4716-A357-8170C4EB1C32} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{dfb3f30a-21b6-481a-be52-e7fabdb75794}*SharedItemsImports = 5 + ..\..\tools\illink\src\ILLink.Shared\ILLink.Shared.projitems*{e579b589-d9c9-4a77-bc2c-a8ead293a603}*SharedItemsImports = 5 + ..\System.Private.CoreLib\src\System.Private.CoreLib.Shared.projitems*{f454fcee-b5be-406d-a64f-fbfbcab3ee31}*SharedItemsImports = 5 + EndGlobalSection EndGlobal diff --git a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs index 85f6e875a824ca..c69e5f3f3bef34 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs @@ -1,9 +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.Http; -using System.Net.Test.Common; using System.Threading; using System.Threading.Tasks; @@ -26,14 +23,11 @@ public HttpClientAbortTest(ITestOutputHelper output) : base(output) { } protected override bool UseHttpClient => true; } - public class AbortTest : ClientWebSocketTestBase + public abstract class AbortTestBase : ClientWebSocketTestBase { - public AbortTest(ITestOutputHelper output) : base(output) { } - + public AbortTestBase(ITestOutputHelper output) : base(output) { } - [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()) { @@ -53,9 +47,7 @@ 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) => { @@ -73,9 +65,7 @@ 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) => { @@ -97,9 +87,7 @@ await cws.SendAsync( }, 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) => { @@ -121,9 +109,7 @@ 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) => { @@ -145,4 +131,31 @@ await cws.SendAsync( }, server); } } + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + public class AbortTest : AbortTestBase + { + public AbortTest(ITestOutputHelper output) : base(output) { } + + [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); + } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs index a38b11d2321c87..ee89e4da0a47e0 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.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.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -24,14 +23,11 @@ public HttpClientCancelTest(ITestOutputHelper output) : base(output) { } protected override bool UseHttpClient => true; } - public class CancelTest : ClientWebSocketTestBase + public abstract class CancelTestBase : ClientWebSocketTestBase { - public CancelTest(ITestOutputHelper output) : base(output) { } + public CancelTestBase(ITestOutputHelper output) : base(output) { } - [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()) { @@ -45,9 +41,7 @@ public async Task ConnectAsync_Cancel_ThrowsCancellationException(Uri server) } } - [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) => { @@ -60,9 +54,7 @@ await TestCancellation((cws) => }, 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) => { @@ -82,9 +74,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) => { @@ -104,9 +94,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) => { @@ -127,9 +115,7 @@ 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)) { @@ -143,9 +129,7 @@ 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)) { @@ -159,9 +143,7 @@ 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)) { @@ -181,4 +163,44 @@ public async Task ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketEx } } } + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + public class CancelTest : CancelTestBase + { + public CancelTest(ITestOutputHelper output) : base(output) { } + + [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); + } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs index 14affae6bd39e5..5fe54f27ef6b07 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs @@ -2,13 +2,10 @@ // 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; @@ -29,15 +26,11 @@ public HttpClientCloseTest(ITestOutputHelper output) : base(output) { } protected override bool UseHttpClient => true; } - public class CloseTest : ClientWebSocketTestBase + public abstract class CloseTestBase : ClientWebSocketTestBase { - public CloseTest(ITestOutputHelper output) : base(output) { } - + public CloseTestBase(ITestOutputHelper output) : base(output) { } - [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"; @@ -84,9 +77,7 @@ await cws.SendAsync( } } - [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)) { @@ -106,9 +97,7 @@ 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); @@ -120,9 +109,7 @@ 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); @@ -145,9 +132,7 @@ 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)) { @@ -164,9 +149,7 @@ 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)) { @@ -180,9 +163,7 @@ 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)) { @@ -199,9 +180,7 @@ 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)) { @@ -224,9 +203,7 @@ 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!"; @@ -262,10 +239,7 @@ 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"; @@ -319,10 +293,7 @@ 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; @@ -367,9 +338,7 @@ 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)) { @@ -397,9 +366,7 @@ await cws.SendAsync( } } - [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)) { @@ -412,10 +379,7 @@ 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)) @@ -450,9 +414,7 @@ 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)) @@ -524,4 +486,76 @@ await Assert.ThrowsAnyAsync(async () => }), new LoopbackServer.Options { WebSocketEndpoint = true }); } } + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + public class CloseTest : CloseTestBase + { + public CloseTest(ITestOutputHelper output) : base(output) { } + + [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + [Theory, MemberData(nameof(EchoServersAndBoolean))] + public Task CloseAsync_ServerInitiatedClose_Success(Uri server, bool useCloseOutputAsync) + => RunClient_CloseAsync_ServerInitiatedClose_Success(server, useCloseOutputAsync); + + [Theory, MemberData(nameof(EchoServers))] + public Task CloseAsync_ClientInitiatedClose_Success(Uri server) + => RunClient_CloseAsync_ClientInitiatedClose_Success(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task CloseAsync_CloseDescriptionIsMaxLength_Success(Uri server) + => RunClient_CloseAsync_CloseDescriptionIsMaxLength_Success(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException(Uri server) + => RunClient_CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task CloseAsync_CloseDescriptionHasUnicode_Success(Uri server) + => RunClient_CloseAsync_CloseDescriptionHasUnicode_Success(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task CloseAsync_CloseDescriptionIsNull_Success(Uri server) + => RunClient_CloseAsync_CloseDescriptionIsNull_Success(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task CloseOutputAsync_ExpectedStates(Uri server) + => RunClient_CloseOutputAsync_ExpectedStates(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task CloseAsync_CloseOutputAsync_Throws(Uri server) + => RunClient_CloseAsync_CloseOutputAsync_Throws(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(Uri server) + => RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanClose(server); + + [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + [Theory, MemberData(nameof(EchoServersAndBoolean))] + public Task CloseOutputAsync_ServerInitiated_CanReceive(Uri server, bool delayReceiving) + => RunClient_CloseOutputAsync_ServerInitiated_CanReceive(server, delayReceiving); + + [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + [Theory, MemberData(nameof(EchoServers))] + public Task CloseOutputAsync_ServerInitiated_CanSend(Uri server) + => RunClient_CloseOutputAsync_ServerInitiated_CanSend(server); + + [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + [Theory, 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); + } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs index cb5f0f978f761e..bdbbd46f8742ef 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs @@ -13,7 +13,7 @@ namespace System.Net.WebSockets.Client.Tests { - public sealed class InvokerConnectTest : ConnectTest + public sealed class InvokerConnectTest : ConnectTestBase { public InvokerConnectTest(ITestOutputHelper output) : base(output) { } @@ -90,21 +90,18 @@ public async Task ConnectAsync_CustomInvokerWithIncompatibleWebSocketOptions_Thr } } - public sealed class HttpClientConnectTest : ConnectTest + public sealed class HttpClientConnectTest : ConnectTestBase { public HttpClientConnectTest(ITestOutputHelper output) : base(output) { } protected override bool UseHttpClient => true; } - public class ConnectTest : ClientWebSocketTestBase + public abstract class ConnectTestBase : ClientWebSocketTestBase { - public ConnectTest(ITestOutputHelper output) : base(output) { } + public ConnectTestBase(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) + protected async Task RunClient_ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(Uri server, string exceptionMessage, WebSocketError errorCode) { using (var cws = new ClientWebSocket()) { @@ -127,24 +124,17 @@ public async Task ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMe } } - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] - [ConditionalTheory(nameof(WebSocketsSupported)), MemberData(nameof(EchoServers))] - public async Task EchoBinaryMessage_Success(Uri server) + protected async Task RunClient_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) + protected async Task RunClient_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()) { @@ -202,10 +192,7 @@ await LoopbackServer.CreateClientAndServerAsync(async uri => }), 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()) { @@ -252,10 +239,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"; @@ -275,9 +259,7 @@ public async Task ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketE } } - [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"; @@ -297,28 +279,7 @@ public async Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_Connectio } } - [ConditionalFact(nameof(WebSocketsSupported))] - [SkipOnPlatform(TestPlatforms.Browser, "SetRequestHeader not supported on Browser")] - 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 }); - } - - [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) + protected async Task RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(Uri server) { using (var cws = new ClientWebSocket()) using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) @@ -343,6 +304,24 @@ public async Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(Uri se } } + [ConditionalFact(nameof(WebSocketsSupported))] + [SkipOnPlatform(TestPlatforms.Browser, "SetRequestHeader not supported on Browser")] + 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 }); + } + [ConditionalFact(nameof(WebSocketsSupported))] public async Task ConnectAsync_CancellationRequestedBeforeConnect_ThrowsOperationCanceledException() { @@ -464,4 +443,48 @@ await LoopbackServer.CreateClientAndServerAsync(async uri => }), new LoopbackServer.Options { WebSocketEndpoint = true }); } } + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + public class ConnectTest : ConnectTestBase + { + public ConnectTest(ITestOutputHelper output) : base(output) { } + + [ActiveIssue("https://github.com/dotnet/runtime/issues/1895")] + [Theory, MemberData(nameof(UnavailableWebSocketServers))] + public Task ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(Uri server, string exceptionMessage, WebSocketError errorCode) + => RunClient_ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(server, exceptionMessage, errorCode); + + [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); + } } 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..6bd1fd196519be 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs @@ -1,11 +1,8 @@ // 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; @@ -35,7 +32,6 @@ public Task KeepAlive_LongDelayBetweenSendReceives_Succeeds(bool useSsl) var options = new LoopbackWebSocketServer.Options(HttpVersion, useSsl, GetInvoker()) { - DisposeServerWebSocket = true, DisposeClientWebSocket = true, ConfigureClientOptions = clientOptions => { 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 dc1da08450622a..2a919586d885bf 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs @@ -145,6 +145,7 @@ public record class Options(Version HttpVersion, bool UseSsl, HttpMessageInvoker public bool DisposeClientWebSocket { get; set; } public bool DisposeHttpInvoker { get; set; } public bool ManualServerHandshakeResponse { get; set; } + public bool IgnoreServerErrors { get; set; } public Action? ConfigureClientOptions { get; set; } } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs index 357dcb0945d665..24c0b025a41c45 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.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.Net.Http; using System.Net.Sockets; using System.Net.Test.Common; using System.Threading; @@ -72,16 +71,14 @@ protected override Task SendAsync(WebSocket ws, ArraySegment arraySegment, ws.SendAsync(arraySegment, messageType, endOfMessage, cancellationToken); } - public abstract class SendReceiveTest : ClientWebSocketTestBase + public abstract class SendReceiveTestBase : 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) { } + public SendReceiveTestBase(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]; @@ -116,10 +113,7 @@ 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) { var sendBuffer = new byte[ushort.MaxValue + 1]; Random.Shared.NextBytes(sendBuffer); @@ -158,9 +152,7 @@ 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)) { @@ -186,10 +178,7 @@ 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)) { @@ -246,11 +235,7 @@ 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 async Task RunClient_ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(Uri server) { using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) { @@ -312,9 +297,7 @@ 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)) { @@ -351,9 +334,7 @@ await SendAsync( } } - [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)) { @@ -391,9 +372,7 @@ 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)) { @@ -420,10 +399,7 @@ 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() + protected async Task RunClient_SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated() { var options = new LoopbackServer.Options { WebSocketEndpoint = true }; @@ -490,9 +466,7 @@ await LoopbackServer.CreateServerAsync(async (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)) { @@ -527,8 +501,58 @@ 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); } } } + + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + public abstract class SendReceiveTest : SendReceiveTestBase + { + public SendReceiveTest(ITestOutputHelper output) : base(output) { } + + [Theory, MemberData(nameof(EchoServers))] + public Task SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(Uri server) + => RunClient_SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(server); + + [SkipOnPlatform(TestPlatforms.Browser, "JS Websocket does not support see issue https://github.com/dotnet/runtime/issues/46983")] + [Theory, MemberData(nameof(EchoServers))] + public Task SendReceive_PartialMessageBeforeCompleteMessageArrives_Success(Uri server) + => RunClient_SendReceive_PartialMessageBeforeCompleteMessageArrives_Success(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage(Uri server) + => RunClient_SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task SendAsync_MultipleOutstandingSendOperations_Throws(Uri server) + => RunClient_SendAsync_MultipleOutstandingSendOperations_Throws(server); + + [ActiveIssue("https://github.com/dotnet/runtime/issues/83517", typeof(PlatformDetection), nameof(PlatformDetection.IsNodeJS))] + [Theory, MemberData(nameof(EchoServers))] + public Task ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(Uri server) + => RunClient_ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(Uri server) + => RunClient_SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task SendReceive_VaryingLengthBuffers_Success(Uri server) + => RunClient_SendReceive_VaryingLengthBuffers_Success(server); + + [Theory, MemberData(nameof(EchoServers))] + public Task SendReceive_Concurrent_Success(Uri server) + => RunClient_SendReceive_Concurrent_Success(server); + + [ActiveIssue("https://github.com/dotnet/runtime/issues/54153", TestPlatforms.Browser)] + [Fact] + public Task SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated() + => RunClient_SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated(); + + [Theory, MemberData(nameof(EchoServers))] + public Task ZeroByteReceive_CompletesWhenDataAvailable(Uri server) + => RunClient_ZeroByteReceive_CompletesWhenDataAvailable(server); + } } From 4cb199a5a056dca7ad8de9931d726b8a5ce2611a Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Mon, 24 Feb 2025 22:05:37 +0000 Subject: [PATCH 03/20] WIP --- .../Handlers/EchoWebSocketHandler.cs | 32 +----- .../Handlers/EchoWebSocketHeadersHandler.cs | 16 +-- .../Helpers/WebSocketAcceptHelper.cs | 31 ++++++ .../Helpers/WebSocketEchoHelper.cs | 66 ++++++----- .../Helpers/WebSocketEchoOptions.cs | 2 +- .../NetCoreServer/NetCoreServer.csproj | 1 + .../tests/AbortTest.Loopback.cs | 57 ++++++++-- .../tests/CancelTest.cs | 6 +- .../tests/ClientWebSocketTestBase.cs | 103 ++++++++++++++---- .../tests/CloseTest.cs | 30 ++--- .../tests/ConnectTest.Http2.cs | 20 ++-- .../tests/ConnectTest.cs | 4 +- .../tests/KeepAliveTest.Loopback.cs | 11 +- .../LoopbackServer/LoopbackWebSocketServer.cs | 89 ++++++++++++--- .../WebSocketHandshakeHelper.cs | 28 +++-- .../LoopbackServer/WebSocketRequestData.cs | 2 + .../tests/SendReceiveTest.Http2.cs | 4 +- .../tests/SendReceiveTest.cs | 16 +-- .../tests/WebSocketHelper.cs | 5 +- 19 files changed, 362 insertions(+), 161 deletions(-) create mode 100644 src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketAcceptHelper.cs 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 5318730438204f..977c05810d1aea 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,7 +3,7 @@ using System; using System.Net.WebSockets; -using System.Net.WebSockets.Tests; +using System.Net.Test.Common; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -15,37 +15,17 @@ public class EchoWebSocketHandler { public static async Task InvokeAsync(HttpContext context) { - WebSocketEchoOptions options = WebSocketEchoOptions.Parse(context.Request.QueryString.Value); - if (options.Delay is TimeSpan d) - { - await Task.Delay(d); - } - + 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(options.SubProtocol)) - { - socket = await context.WebSockets.AcceptWebSocketAsync(options.SubProtocol); - } - else - { - socket = await context.WebSockets.AcceptWebSocketAsync(); - } - - await WebSocketEchoHelper.ProcessRequest( - socket, - options.ReplyWithPartialMessages, - options.ReplyWithEnhancedCloseMessage); + await WebSocketEchoHelper.RunEchoAll( + socket, options.ReplyWithPartialMessages, options.ReplyWithEnhancedCloseMessage); } catch (Exception) { 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 5307d394815f25..51ced55c384258 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 @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Net.WebSockets; -using System.Net.WebSockets.Tests; +using System.Net.Test.Common; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -20,20 +20,16 @@ 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 WebSocketEchoHelper.ProcessHeadersRequest( - socket, - context.Request.Headers.Select(h => new KeyValuePair(h.Key, h.Value.ToString()))); + var headers = context.Request.Headers.Select( + h => new KeyValuePair(h.Key, h.Value.ToString())) + await WebSocketEchoHelper.RunEchoHeaders(socket, headers); } catch (Exception) { 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..713071691047cf --- /dev/null +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketAcceptHelper.cs @@ -0,0 +1,31 @@ +// 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.Text; +using System.Threading; +using System.Threading.Tasks; + +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/Helpers/WebSocketEchoHelper.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs index 15bb6b9a76c00e..f54d21fb535ea2 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs @@ -2,31 +2,31 @@ // 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.WebSockets.Tests +namespace System.Net.Test.Common { public static class WebSocketEchoHelper { - private const int MaxBufferSize = 128 * 1024; - private const int HeadersBufferSize = 1024; - - public static async Task ProcessRequest(WebSocket socket, bool replyWithPartialMessages, bool replyWithEnhancedCloseMessage) + 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.None); + 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.None); + await socket.CloseAsync(WebSocketCloseStatus.Empty, null, cancellationToken); } else { @@ -36,7 +36,7 @@ await socket.CloseAsync( replyWithEnhancedCloseMessage ? ("Server received: " + (int)closeStatus + " " + receiveResult.CloseStatusDescription) : receiveResult.CloseStatusDescription, - CancellationToken.None); + cancellationToken); } continue; @@ -50,13 +50,13 @@ await socket.CloseAsync( { receiveResult = await socket.ReceiveAsync( new ArraySegment(receiveBuffer, offset, MaxBufferSize - offset), - CancellationToken.None); + cancellationToken); } else { receiveResult = await socket.ReceiveAsync( new ArraySegment(throwAwayBuffer), - CancellationToken.None); + cancellationToken); } offset += receiveResult.Count; @@ -67,8 +67,8 @@ await socket.CloseAsync( { await socket.CloseAsync( WebSocketCloseStatus.MessageTooBig, - String.Format("{0}: {1} > {2}", WebSocketCloseStatus.MessageTooBig.ToString(), offset, MaxBufferSize), - CancellationToken.None); + string.Format("{0}: {1} > {2}", WebSocketCloseStatus.MessageTooBig.ToString(), offset, MaxBufferSize), + cancellationToken); continue; } @@ -80,11 +80,11 @@ await socket.CloseAsync( receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, offset); if (receivedMessage == ".close") { - await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None); + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); } else if (receivedMessage == ".shutdown") { - await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None); + await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); } else if (receivedMessage == ".abort") { @@ -96,15 +96,14 @@ await socket.CloseAsync( } 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); + 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.None); - await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None); + cancellationToken); + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); } else if (socket.State == WebSocketState.Open) { @@ -122,22 +121,23 @@ await socket.SendAsync( new ArraySegment(receiveBuffer, 0, offset), receiveResult.MessageType, !replyWithPartialMessages, - CancellationToken.None); + cancellationToken); } if (receivedMessage == ".closeafter") { - await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None); + await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); } else if (receivedMessage == ".shutdownafter") { - await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, CancellationToken.None); + await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); } } } - public static async Task ProcessHeadersRequest(WebSocket socket, IEnumerable> headers) + public static async Task RunEchoHeaders(WebSocket socket, IEnumerable> headers, CancellationToken cancellationToken = default) { - var receiveBuffer = new byte[HeadersBufferSize]; + const int MaxBufferSize = 1024; + var receiveBuffer = new byte[MaxBufferSize]; // Reflect all headers and cookies var sb = new StringBuilder(); @@ -151,29 +151,39 @@ public static async Task ProcessHeadersRequest(WebSocket socket, IEnumerable(sendBuffer), WebSocketMessageType.Text, true, new CancellationToken()); + 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.None); + 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.None); + await socket.CloseAsync(WebSocketCloseStatus.Empty, null, cancellationToken); } else { await socket.CloseAsync( receiveResult.CloseStatus.GetValueOrDefault(), receiveResult.CloseStatusDescription, - CancellationToken.None); + 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/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs index 5237867716f77c..687401bc665a31 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; -namespace System.Net.WebSockets.Tests +namespace System.Net.Test.Common { public struct WebSocketEchoOptions { 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 f13f797618a71a..22c178aa9b562f 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/NetCoreServer.csproj +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/NetCoreServer.csproj @@ -47,6 +47,7 @@ + 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 95b14197c48aa2..c07f6cc001c6c4 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs @@ -14,12 +14,10 @@ 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_Loopback : AbortTestBase { public AbortTest_Loopback(ITestOutputHelper output) : base(output) { } - protected virtual Version HttpVersion => Net.HttpVersion.Version11; - public static object[][] AbortClient_MemberData = ToMemberData(Enum.GetValues(), UseSsl_Values, /* verifySendReceive */ Bool_Values); [Theory] @@ -65,7 +63,13 @@ 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 = HttpVersion, + UseSsl = useSsl, + HttpInvoker = GetInvoker(), + DisposeServerWebSocket = true + }, timeoutCts.Token); } @@ -82,10 +86,12 @@ public Task ServerPrematureEos_ClientGetsCorrectException(ServerEosType serverEo var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); - var globalOptions = new LoopbackWebSocketServer.Options(HttpVersion, useSsl, HttpInvoker: null) + var globalOptions = new LoopbackWebSocketServer.Options { - DisposeServerWebSocket = false, - ManualServerHandshakeResponse = true + HttpVersion = HttpVersion, + UseSsl = useSsl, + HttpInvoker = null, + SkipServerHandshakeResponse = true }; var serverReceivedEosTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -184,6 +190,39 @@ protected static async Task VerifySendReceiveAsync(WebSocket ws, byte[] localMsg await sendTask.ConfigureAwait(false); await remoteAck.WaitAsync(cancellationToken).ConfigureAwait(false); } + + /*[Theory, MemberData(nameof(UseSsl_MemberData))] + public Task Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage(bool useSsl) + { + var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); + + return LoopbackWebSocketServer.RunEchoAsync( + RunClient_Abort_SendAndAbort_Success, + new LoopbackWebSocketServer.Options + { + HttpVersion = HttpVersion, + UseSsl = useSsl, + HttpInvoker = GetInvoker(), + IgnoreServerErrors = true + }, + timeoutCts.Token); + }*/ + + /*//[Theory, MemberData(nameof(EchoServers))] + public Task Abort_SendAndAbort_Success() + => RunClient_Abort_SendAndAbort_Success(server); + + //[Theory, MemberData(nameof(EchoServers))] + public Task Abort_ReceiveAndAbort_Success() + => RunClient_Abort_ReceiveAndAbort_Success(server); + + //[Theory, MemberData(nameof(EchoServers))] + public Task Abort_CloseAndAbort_Success() + => RunClient_Abort_CloseAndAbort_Success(server); + + //[Theory, MemberData(nameof(EchoServers))] + public Task ClientWebSocket_Abort_CloseOutputAsync() + => RunClient_ClientWebSocket_Abort_CloseOutputAsync(server);*/ } // --- HTTP/1.1 WebSocket loopback tests --- @@ -210,7 +249,7 @@ public AbortTest_SharedHandler_Loopback(ITestOutputHelper output) : base(output) public class AbortTest_Invoker_Http2 : AbortTest_Invoker_Loopback { public AbortTest_Invoker_Http2(ITestOutputHelper output) : base(output) { } - protected override Version HttpVersion => Net.HttpVersion.Version20; + internal 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); } @@ -218,7 +257,7 @@ protected override Task SendServerResponseAndEosAsync(WebSocketRequestData rd, S public class AbortTest_HttpClient_Http2 : AbortTest_HttpClient_Loopback { public AbortTest_HttpClient_Http2(ITestOutputHelper output) : base(output) { } - protected override Version HttpVersion => Net.HttpVersion.Version20; + internal 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); } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs index ee89e4da0a47e0..844aa5d62705a9 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs @@ -117,7 +117,7 @@ await cws.SendAsync( 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); @@ -131,7 +131,7 @@ protected async Task RunClient_ReceiveAsync_CancelThenReceive_ThrowsOperationCan 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); @@ -145,7 +145,7 @@ protected async Task RunClient_ReceiveAsync_ReceiveThenCancel_ThrowsOperationCan 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); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs index 4e4fb4b3d87c76..ff6d28c8c40092 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; +using Microsoft.DotNet.XUnitExtensions; using TestUtilities; using Xunit; @@ -75,7 +76,7 @@ public static IEnumerable UnavailableWebSocketServers public async Task TestCancellation(Func action, Uri server) { - using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { try { @@ -118,6 +119,18 @@ protected static async Task ReceiveEntireMessageAsync(We } } + protected TestConfig _defaultConfig = null!; + protected TestConfig DefaultConfig => _defaultConfig ??= new TestConfig + { + InvokerType = UseCustomInvoker + ? HttpInvokerType.HttpMessageInvoker + : UseHttpClient + ? HttpInvokerType.HttpClient + : HttpInvokerType.Shared, + ConfigureHttpHandler = ConfigureCustomHandler, + HttpVersion = HttpVersion, + }; + protected virtual bool UseCustomInvoker => false; protected virtual bool UseHttpClient => false; @@ -126,40 +139,88 @@ protected static async Task ReceiveEntireMessageAsync(We protected Action? ConfigureCustomHandler; - internal HttpMessageInvoker? GetInvoker() + internal virtual Version HttpVersion => Net.HttpVersion.Version11; + + internal HttpMessageInvoker? GetInvoker() => DefaultConfig.Invoker; + + protected Task GetConnectedWebSocket(Uri uri, int timeOutMilliseconds, ITestOutputHelper output) + => WebSocketHelper.GetConnectedWebSocket(uri, timeOutMilliseconds, output, o => ConfigureHttpVersion(o, uri), GetInvoker()); + + protected Task GetConnectedWebSocket(Uri uri) + => GetConnectedWebSocket(uri, TimeOutMilliseconds, _output); + + protected Task ConnectAsync(ClientWebSocket cws, Uri uri, CancellationToken cancellationToken) { - var handler = new HttpClientHandler(); + ConfigureHttpVersion(cws.Options, uri); + return cws.ConnectAsync(uri, GetInvoker(), cancellationToken); + } - if (PlatformDetection.IsNotBrowser) + protected Task TestEcho(Uri uri, WebSocketMessageType type) + => WebSocketHelper.TestEcho(uri, type, TimeOutMilliseconds, _output,o => ConfigureHttpVersion(o, uri), GetInvoker()); + + protected void ConfigureHttpVersion(ClientWebSocketOptions options, Uri uri) + { + if (PlatformDetection.IsBrowser) { - handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + if (HttpVersion != Net.HttpVersion.Version11) + { + throw new SkipTestException($"HTTP version {HttpVersion} is not supported for WebSockets on Browser."); + } + return; } - ConfigureCustomHandler?.Invoke(handler); + options.HttpVersion = HttpVersion; + options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; - if (UseCustomInvoker) + if (HttpVersion == Net.HttpVersion.Version20 && uri.Query is not null or "" or "?") { - Debug.Assert(!UseHttpClient); - return new HttpMessageInvoker(handler); + // HTTP/2 CONNECT requests drop path and query from the request URI, + // see https://datatracker.ietf.org/doc/html/rfc7540#section-8.3: + // > The ":scheme" and ":path" pseudo-header fields MUST be omitted. + // Saving the original query string in a custom header. + options.SetRequestHeader(WebSocketHelper.OriginalQueryStringHeader, uri.Query); } + } + + public static bool WebSocketsSupported { get { return WebSocketHelper.WebSocketsSupported; } } - if (UseHttpClient) + public record class TestConfig + { + public HttpInvokerType InvokerType { get; set; } + public Action? ConfigureHttpHandler { get; set; } + public HttpMessageInvoker? Invoker => CreateInvoker(); + + public Version HttpVersion { get; set; } + public bool? UseSsl { get; set; } + public Uri? Uri { get; set; } + + private HttpMessageInvoker? CreateInvoker() { - return new HttpClient(handler); - } + var handler = new HttpClientHandler(); - return null; - } + if (PlatformDetection.IsNotBrowser) + { + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + } - protected Task GetConnectedWebSocket(Uri uri, int TimeOutMilliseconds, ITestOutputHelper output) => - WebSocketHelper.GetConnectedWebSocket(uri, TimeOutMilliseconds, output, invoker: GetInvoker()); + ConfigureHttpHandler?.Invoke(handler); - protected Task ConnectAsync(ClientWebSocket cws, Uri uri, CancellationToken cancellationToken) => - cws.ConnectAsync(uri, GetInvoker(), cancellationToken); + return InvokerType switch + { + HttpInvokerType.Shared => null, + HttpInvokerType.HttpClient => new HttpClient(handler), + HttpInvokerType.HttpMessageInvoker => new HttpMessageInvoker(handler), + _ => throw new NotImplementedException() + }; + } - protected Task TestEcho(Uri uri, WebSocketMessageType type, int timeOutMilliseconds, ITestOutputHelper output) => - WebSocketHelper.TestEcho(uri, WebSocketMessageType.Text, TimeOutMilliseconds, _output, GetInvoker()); + } - public static bool WebSocketsSupported { get { return WebSocketHelper.WebSocketsSupported; } } + public enum HttpInvokerType + { + Shared = 0, + HttpClient, + HttpMessageInvoker + } } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs index 5fe54f27ef6b07..42c795f879bef0 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs @@ -34,7 +34,7 @@ protected async Task RunClient_CloseAsync_ServerInitiatedClose_Success(Uri serve { const string shutdownWebSocketMetaCommand = ".shutdown"; - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); @@ -79,7 +79,7 @@ await cws.SendAsync( 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); @@ -101,7 +101,7 @@ protected async Task RunClient_CloseAsync_CloseDescriptionIsMaxLength_Success(Ur { 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); @@ -113,7 +113,7 @@ protected async Task RunClient_CloseAsync_CloseDescriptionIsMaxLengthPlusOne_Thr { 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); @@ -134,7 +134,7 @@ protected async Task RunClient_CloseAsync_CloseDescriptionIsMaxLengthPlusOne_Thr 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); @@ -151,7 +151,7 @@ protected async Task RunClient_CloseAsync_CloseDescriptionHasUnicode_Success(Uri 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); @@ -165,7 +165,7 @@ protected async Task RunClient_CloseAsync_CloseDescriptionIsNull_Success(Uri ser 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); @@ -182,7 +182,7 @@ protected async Task RunClient_CloseOutputAsync_ExpectedStates(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); @@ -207,7 +207,7 @@ protected async Task RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanCl { string message = "Hello WebSockets!"; - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); @@ -244,7 +244,7 @@ protected async Task RunClient_CloseOutputAsync_ServerInitiated_CanReceive(Uri s var expectedCloseStatus = WebSocketCloseStatus.NormalClosure; var expectedCloseDescription = ".shutdownafter"; - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); @@ -299,7 +299,7 @@ protected async Task RunClient_CloseOutputAsync_ServerInitiated_CanSend(Uri serv var expectedCloseStatus = WebSocketCloseStatus.NormalClosure; var expectedCloseDescription = ".shutdown"; - using (ClientWebSocket cws = await GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); @@ -340,7 +340,7 @@ await cws.SendAsync( 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( @@ -368,7 +368,7 @@ await cws.SendAsync( 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); @@ -382,7 +382,7 @@ protected async Task RunClient_CloseOutputAsync_CloseDescriptionIsNull_Success(U 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); @@ -417,7 +417,7 @@ protected async Task RunClient_CloseOutputAsync_DuringConcurrentReceiveAsync_Exp 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); 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 9de9b934b59986..d7f72532a8c2c1 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs @@ -32,8 +32,8 @@ public HttpClientConnectTest_Http2_NoInvoker(ITestOutputHelper output) : base(ou 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 = 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); @@ -69,7 +69,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); @@ -95,7 +95,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); @@ -120,7 +120,7 @@ public async Task ConnectAsync_Http11Server_DowngradeFail() 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(Test.Common.Configuration.WebSockets.SecureRemoteEchoServer, GetInvoker(), cts.Token); @@ -145,7 +145,7 @@ 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.HttpVersion = Net.HttpVersion.Version20; cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; await cws.ConnectAsync(server, GetInvoker(), cts.Token); Assert.Equal(WebSocketState.Open, cws.State); @@ -161,7 +161,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); } @@ -183,7 +183,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); } @@ -203,11 +203,11 @@ public async Task ConnectAsync_SameHttp2ConnectionUsedForMultipleWebSocketConnec 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); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs index bdbbd46f8742ef..1a0a30868292ff 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs @@ -126,12 +126,12 @@ protected async Task RunClient_ConnectAsync_NotWebSocketServer_ThrowsWebSocketEx protected async Task RunClient_EchoBinaryMessage_Success(Uri server) { - await TestEcho(server, WebSocketMessageType.Binary, TimeOutMilliseconds, _output); + await TestEcho(server, WebSocketMessageType.Binary); } protected async Task RunClient_EchoTextMessage_Success(Uri server) { - await TestEcho(server, WebSocketMessageType.Text, TimeOutMilliseconds, _output); + await TestEcho(server, WebSocketMessageType.Text); } protected async Task RunClient_ConnectAsync_AddCustomHeaders_Success(Uri server) 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 6bd1fd196519be..8d948c5fb3a7f6 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs @@ -14,8 +14,6 @@ public abstract class KeepAliveTest_Loopback : ClientWebSocketTestBase { public KeepAliveTest_Loopback(ITestOutputHelper output) : base(output) { } - protected virtual Version HttpVersion => Net.HttpVersion.Version11; - [OuterLoop("Uses Task.Delay")] [Theory] [MemberData(nameof(UseSsl_MemberData))] @@ -30,8 +28,11 @@ public Task KeepAlive_LongDelayBetweenSendReceives_Succeeds(bool useSsl) var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); - var options = new LoopbackWebSocketServer.Options(HttpVersion, useSsl, GetInvoker()) + var options = new LoopbackWebSocketServer.Options { + HttpVersion = HttpVersion, + UseSsl = useSsl, + HttpInvoker = GetInvoker(), DisposeClientWebSocket = true, ConfigureClientOptions = clientOptions => { @@ -129,12 +130,12 @@ public KeepAliveTest_SharedHandler_Loopback(ITestOutputHelper output) : base(out public class KeepAliveTest_Invoker_Http2 : KeepAliveTest_Invoker_Loopback { public KeepAliveTest_Invoker_Http2(ITestOutputHelper output) : base(output) { } - protected override Version HttpVersion => Net.HttpVersion.Version20; + internal override Version HttpVersion => Net.HttpVersion.Version20; } public class KeepAliveTest_HttpClient_Http2 : KeepAliveTest_HttpClient_Loopback { public KeepAliveTest_HttpClient_Http2(ITestOutputHelper output) : base(output) { } - protected override Version HttpVersion => Net.HttpVersion.Version20; + internal override Version HttpVersion => Net.HttpVersion.Version20; } } 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 2a919586d885bf..477768986c709c 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs @@ -17,7 +17,7 @@ public static Task RunAsync( Options options, CancellationToken cancellationToken) { - Assert.False(options.ManualServerHandshakeResponse, "Not supported in this overload"); + Assert.False(options.SkipServerHandshakeResponse, "Not supported in this overload"); return RunAsyncPrivate( uri => RunClientAsync(uri, clientWebSocketFunc, options, cancellationToken), @@ -40,13 +40,21 @@ public static Task RunAsync( return RunAsyncPrivate(loopbackClientFunc, loopbackServerFunc, options, cancellationToken); } + public static Task RunEchoAsync(Func loopbackClientFunc, Options options, CancellationToken cancellationToken) + { + Assert.False(options.DisposeClientWebSocket, "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, (data, token) => RunEchoServerAsync(data, options, token), options, cancellationToken); + } + private static Task RunAsyncPrivate( Func loopbackClientFunc, Func loopbackServerFunc, Options options, CancellationToken cancellationToken) { - bool sendDefaultServerHandshakeResponse = !options.ManualServerHandshakeResponse; if (options.HttpVersion == HttpVersion.Version11) { return LoopbackServer.CreateClientAndServerAsync( @@ -55,7 +63,7 @@ private static Task RunAsyncPrivate( { await server.AcceptConnectionAsync(async connection => { - var requestData = await WebSocketHandshakeHelper.ProcessHttp11RequestAsync(connection, sendDefaultServerHandshakeResponse, cancellationToken).ConfigureAwait(false); + var requestData = await WebSocketHandshakeHelper.ProcessHttp11RequestAsync(connection, options.SkipServerHandshakeResponse, cancellationToken).ConfigureAwait(false); await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); }); }, @@ -67,7 +75,7 @@ await server.AcceptConnectionAsync(async connection => loopbackClientFunc, async server => { - var requestData = await WebSocketHandshakeHelper.ProcessHttp2RequestAsync(server, sendDefaultServerHandshakeResponse, cancellationToken).ConfigureAwait(false); + var requestData = await WebSocketHandshakeHelper.ProcessHttp2RequestAsync(server, options.SkipServerHandshakeResponse, cancellationToken).ConfigureAwait(false); var http2Connection = requestData.Http2Connection!; var http2StreamId = requestData.Http2StreamId.Value; @@ -89,14 +97,63 @@ private static async Task RunServerAsync( Options options, CancellationToken cancellationToken) { - var wsOptions = new WebSocketCreationOptions { IsServer = true }; - var serverWebSocket = WebSocket.CreateFromStream(requestData.TransportStream, wsOptions); + WebSocket serverWebSocket = null!; + CancellationTokenRegistration registration = default; + try + { + var wsOptions = new WebSocketCreationOptions { IsServer = true }; + options.ConfigureServerOptions?.Invoke(wsOptions); + + serverWebSocket = WebSocket.CreateFromStream(requestData.TransportStream, wsOptions); + registration = cancellationToken.Register(() => serverWebSocket.Abort()); + + await serverWebSocketFunc(serverWebSocket, cancellationToken).ConfigureAwait(false); + } + catch (Exception) when (options.IgnoreServerErrors) + { + // ignore + } + finally + { + registration.Dispose(); + if (options.DisposeServerWebSocket) + { + serverWebSocket?.Dispose(); + } + } + } - await serverWebSocketFunc(serverWebSocket, cancellationToken).ConfigureAwait(false); + private static async Task RunEchoServerAsync(WebSocketRequestData data, Options options, CancellationToken cancellationToken) + { + try + { + WebSocketEchoOptions echoOptions = await WebSocketEchoHelper.ProcessOptions(data.Query, cancellationToken); - if (options.DisposeServerWebSocket) + if (options.ConfigureServerOptions is not null && echoOptions.SubProtocol is not null) + { + Options copy = options; + options = copy with { + ConfigureServerOptions = o => + { + o.SubProtocol = echoOptions.SubProtocol; + copy.ConfigureServerOptions.Invoke(o); + } + }; + } + + await RunServerAsync( + data, + (serverWebSocket, token) => WebSocketEchoHelper.RunEchoAll( + serverWebSocket, + echoOptions.ReplyWithPartialMessages, + echoOptions.ReplyWithEnhancedCloseMessage, + token), + options, + cancellationToken); + } + catch (Exception) when (options.IgnoreServerErrors) { - serverWebSocket.Dispose(); + // ignore } } @@ -139,13 +196,19 @@ public static async Task GetConnectedClientAsync(Uri uri, Optio return clientWebSocket; } - public record class Options(Version HttpVersion, bool UseSsl, HttpMessageInvoker? HttpInvoker) + public record class Options() { - public bool DisposeServerWebSocket { get; set; } = true; + public Version HttpVersion { get; init; } + public bool UseSsl { get; init; } + public HttpMessageInvoker? HttpInvoker { get; init; } + + public bool DisposeServerWebSocket { get; set; } + public bool SkipServerHandshakeResponse { get; set; } + public bool IgnoreServerErrors { get; set; } + public Action? ConfigureServerOptions { get; set; } + public bool DisposeClientWebSocket { get; set; } public bool DisposeHttpInvoker { get; set; } - public bool ManualServerHandshakeResponse { get; set; } - public bool IgnoreServerErrors { get; set; } public Action? ConfigureClientOptions { 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 f70abe07625f64..66142458a1d05e 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,7 @@ 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, CancellationToken cancellationToken = default) { List headers = await connection.ReadRequestHeaderAsync().WaitAsync(cancellationToken).ConfigureAwait(false); @@ -24,9 +23,18 @@ public static async Task ProcessHttp11RequestAsync(Loopbac Http11Connection = connection }; - foreach (string header in headers.Skip(1)) + // 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) { - string[] tokens = header.Split(new char[] { ':' }, StringSplitOptions.RemoveEmptyEntries); + int spaceIndex = headers[0].IndexOf(' ', queryIndex); + data.Query = headers[0].Substring(queryIndex, spaceIndex - queryIndex); + } + + 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,7 +46,7 @@ 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); } @@ -53,7 +61,7 @@ private static async Task SendHttp11ServerResponseAsync(LoopbackServer.Connectio 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, CancellationToken cancellationToken = default) { var connection = await server.EstablishConnectionAsync(new SettingsEntry { SettingId = SettingId.EnableConnect, Value = 1 }) .WaitAsync(cancellationToken).ConfigureAwait(false); @@ -78,7 +86,13 @@ public static async Task ProcessHttp2RequestAsync(Http2Loo var isValidOpeningHandshake = httpRequestData.Method == HttpMethod.Connect.ToString() && data.Headers.ContainsKey(":protocol"); Assert.True(isValidOpeningHandshake); - if (sendServerResponse) + // HTTP/2 CONNECT requests drop path and query from the request URI, + // see https://datatracker.ietf.org/doc/html/rfc7540#section-8.3: + // > The ":scheme" and ":path" pseudo-header fields MUST be omitted. + // The original query string was passed in a custom header. + data.Query = data.Headers.GetValueOrDefault(WebSocketHelper.OriginalQueryStringHeader); + + if (!skipServerHandshakeResponse) { await SendHttp2ServerResponseAsync(connection, streamId, cancellationToken: cancellationToken).ConfigureAwait(false); } 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 f698d670da3114..fbd838361d53d0 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketRequestData.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketRequestData.cs @@ -10,6 +10,8 @@ namespace System.Net.WebSockets.Client.Tests public class WebSocketRequestData { public Dictionary Headers { get; set; } = new Dictionary(); + public string? Query { get; set; } + public Stream? TransportStream { get; set; } public Version HttpVersion { get; set; } 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..39cd184afebc3e 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Http2.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Http2.cs @@ -40,7 +40,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); @@ -78,7 +78,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); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs index 24c0b025a41c45..426dfb261ccc57 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs @@ -87,7 +87,7 @@ protected async Task RunClient_SendReceive_PartialMessageDueToSmallReceiveBuffer 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); @@ -154,7 +154,7 @@ protected async Task RunClient_SendReceive_PartialMessageBeforeCompleteMessageAr 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); @@ -180,7 +180,7 @@ protected async Task RunClient_SendAsync_SendCloseMessageType_ThrowsArgumentExce 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); @@ -237,7 +237,7 @@ protected async Task RunClient_SendAsync_MultipleOutstandingSendOperations_Throw 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); @@ -299,7 +299,7 @@ await SendAsync( 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"; @@ -336,7 +336,7 @@ await SendAsync( protected async Task RunClient_SendReceive_VaryingLengthBuffers_Success(Uri server) { - using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(server, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var rand = new Random(); var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); @@ -374,7 +374,7 @@ protected async Task RunClient_SendReceive_VaryingLengthBuffers_Success(Uri serv 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); @@ -468,7 +468,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) => 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); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs index df29e843590e9b..14bdecf397387d 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs @@ -13,6 +13,8 @@ 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; } } @@ -21,6 +23,7 @@ public static async Task TestEcho( WebSocketMessageType type, int timeOutMilliseconds, ITestOutputHelper output, + Action configureOptions, HttpMessageInvoker? invoker = null) { var cts = new CancellationTokenSource(timeOutMilliseconds); @@ -29,7 +32,7 @@ public static async Task TestEcho( var receiveBuffer = new byte[100]; var receiveSegment = new ArraySegment(receiveBuffer); - using (ClientWebSocket cws = await GetConnectedWebSocket(server, timeOutMilliseconds, output, invoker: invoker)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server, timeOutMilliseconds, output, configureOptions, invoker)) { output.WriteLine("TestEcho: SendAsync starting."); await cws.SendAsync(WebSocketData.GetBufferFromText(message), type, true, cts.Token); From 8e96f499133976db5bb59e214d0b7bcbf4aca9a8 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Thu, 27 Feb 2025 22:21:54 +0000 Subject: [PATCH 04/20] Fix loopback echo server, add more tests --- .../Helpers/WebSocketEchoHelper.cs | 27 ++ .../Helpers/WebSocketEchoOptions.cs | 19 +- .../tests/AbortTest.Loopback.cs | 101 ++++---- .../tests/AbortTest.cs | 15 ++ .../tests/CancelTest.Loopback.cs | 119 +++++++++ .../tests/ClientWebSocketTestBase.cs | 96 ++++++- .../tests/CloseTest.Loopback.cs | 228 +++++++++++++++++ .../tests/CloseTest.cs | 47 ---- .../tests/ConnectTest.Loopback.cs | 231 +++++++++++++++++ .../tests/ConnectTest.cs | 138 ---------- .../LoopbackServer/LoopbackWebSocketServer.cs | 242 +++++++++++++----- .../WebSocketHandshakeHelper.cs | 65 ++++- .../LoopbackServer/WebSocketRequestData.cs | 2 +- .../System.Net.WebSockets.Client.Tests.csproj | 3 + 14 files changed, 997 insertions(+), 336 deletions(-) create mode 100644 src/libraries/System.Net.WebSockets.Client/tests/CancelTest.Loopback.cs create mode 100644 src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs create mode 100644 src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs index f54d21fb535ea2..744996f0e20e5c 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Diagnostics; using System.Net.WebSockets; using System.Text; using System.Threading; @@ -21,7 +22,9 @@ public static async Task RunEchoAll(WebSocket socket, bool replyWithPartialMessa // Stay in loop while websocket is open while (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseSent) { + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Waiting for messages..."); var receiveResult = await socket.ReceiveAsync(new ArraySegment(receiveBuffer), cancellationToken); + if (receiveResult.MessageType == WebSocketMessageType.Close) { if (receiveResult.CloseStatus == WebSocketCloseStatus.Empty) @@ -46,6 +49,7 @@ await socket.CloseAsync( int offset = receiveResult.Count; while (receiveResult.EndOfMessage == false) { + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Incomplete message, waiting for continuations..."); if (offset < MaxBufferSize) { receiveResult = await socket.ReceiveAsync( @@ -65,10 +69,12 @@ await socket.CloseAsync( // Close socket if the message was too big. if (offset > MaxBufferSize) { + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Full close (MessageTooBig)"); await socket.CloseAsync( WebSocketCloseStatus.MessageTooBig, string.Format("{0}: {1} > {2}", WebSocketCloseStatus.MessageTooBig.ToString(), offset, MaxBufferSize), cancellationToken); + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Closed"); continue; } @@ -78,32 +84,44 @@ await socket.CloseAsync( if (receiveResult.MessageType == WebSocketMessageType.Text) { receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, offset); + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Received message: '{receivedMessage}'"); if (receivedMessage == ".close") { + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Full close..."); await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Closed"); } else if (receivedMessage == ".shutdown") { + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Shutdown writes only..."); await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Writes closed"); } else if (receivedMessage == ".abort") { socket.Abort(); + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Aborted"); } else if (receivedMessage == ".delay5sec") { + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Waiting for 5 seconds without sending any messages."); await Task.Delay(5000); + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Wait completed."); } else if (receivedMessage == ".receiveMessageAfterClose") { string message = $"{receivedMessage} {DateTime.Now:HH:mm:ss}"; + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Sending message: '{message}'"); byte[] buffer = Encoding.UTF8.GetBytes(message); await socket.SendAsync( new ArraySegment(buffer, 0, message.Length), WebSocketMessageType.Text, true, cancellationToken); + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Sent"); + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Full close..."); await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Closed"); } else if (socket.State == WebSocketState.Open) { @@ -112,24 +130,31 @@ await socket.SendAsync( } else { + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Received message type {receiveResult.MessageType}"); sendMessage = true; } if (sendMessage) { + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Echo message"); await socket.SendAsync( new ArraySegment(receiveBuffer, 0, offset), receiveResult.MessageType, !replyWithPartialMessages, cancellationToken); + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Sent"); } if (receivedMessage == ".closeafter") { + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Full close..."); await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Closed"); } else if (receivedMessage == ".shutdownafter") { + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Shutdown writes only..."); await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); + //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Writes closed"); } } } @@ -178,9 +203,11 @@ await socket.CloseAsync( public static async ValueTask ProcessOptions(string queryString, CancellationToken cancellationToken = default) { + //Console.WriteLine($"Server: Processing echo oprions from query string = '{queryString}'"); WebSocketEchoOptions options = WebSocketEchoOptions.Parse(queryString); if (options.Delay is TimeSpan d) { + //Console.WriteLine($"Server: delay = {d}"); await Task.Delay(d, cancellationToken).ConfigureAwait(false); } return options; diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs index 687401bc665a31..c213d36bc35678 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs @@ -1,25 +1,22 @@ // 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.Text; -using System.Threading; -using System.Threading.Tasks; - namespace System.Net.Test.Common { - public struct WebSocketEchoOptions + public readonly struct WebSocketEchoOptions { - public bool ReplyWithPartialMessages { get; set; } - public bool ReplyWithEnhancedCloseMessage { get; set; } - public string SubProtocol { get; set; } - public TimeSpan? Delay { get; set; } + 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 Default; } return new WebSocketEchoOptions 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 c07f6cc001c6c4..530df78923f9e8 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,9 +10,9 @@ 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 : AbortTestBase + public abstract class AbortTest_LoopbackBase : AbortTestBase { - public AbortTest_Loopback(ITestOutputHelper output) : base(output) { } + public AbortTest_LoopbackBase(ITestOutputHelper output) : base(output) { } public static object[][] AbortClient_MemberData = ToMemberData(Enum.GetValues(), UseSsl_Values, /* verifySendReceive */ Bool_Values); @@ -190,75 +186,94 @@ protected static async Task VerifySendReceiveAsync(WebSocket ws, byte[] localMsg await sendTask.ConfigureAwait(false); await remoteAck.WaitAsync(cancellationToken).ConfigureAwait(false); } + } - /*[Theory, MemberData(nameof(UseSsl_MemberData))] - public Task Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage(bool useSsl) - { - var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); + // --- Loopback Echo Server "overrides" --- - return LoopbackWebSocketServer.RunEchoAsync( - RunClient_Abort_SendAndAbort_Success, - new LoopbackWebSocketServer.Options - { - HttpVersion = HttpVersion, - UseSsl = useSsl, - HttpInvoker = GetInvoker(), - IgnoreServerErrors = true - }, - timeoutCts.Token); - }*/ + public abstract class AbortTest_Loopback : AbortTest_LoopbackBase + { + public AbortTest_Loopback(ITestOutputHelper output) : base(output) { } + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage(bool useSsl) => RunEchoAsync( + RunClient_Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage, useSsl); - /*//[Theory, MemberData(nameof(EchoServers))] - public Task Abort_SendAndAbort_Success() - => RunClient_Abort_SendAndAbort_Success(server); + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task Abort_SendAndAbort_Success(bool useSsl) => RunEchoAsync( + RunClient_Abort_SendAndAbort_Success, useSsl); - //[Theory, MemberData(nameof(EchoServers))] - public Task Abort_ReceiveAndAbort_Success() - => RunClient_Abort_ReceiveAndAbort_Success(server); + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task Abort_ReceiveAndAbort_Success(bool useSsl) => RunEchoAsync( + RunClient_Abort_ReceiveAndAbort_Success, useSsl); - //[Theory, MemberData(nameof(EchoServers))] - public Task Abort_CloseAndAbort_Success() - => RunClient_Abort_CloseAndAbort_Success(server); + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task Abort_CloseAndAbort_Success(bool useSsl) => RunEchoAsync( + RunClient_Abort_CloseAndAbort_Success, useSsl); - //[Theory, MemberData(nameof(EchoServers))] - public Task ClientWebSocket_Abort_CloseOutputAsync() - => RunClient_ClientWebSocket_Abort_CloseOutputAsync(server);*/ + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task ClientWebSocket_Abort_CloseOutputAsync(bool useSsl) => RunEchoAsync( + RunClient_ClientWebSocket_Abort_CloseOutputAsync, useSsl); } // --- HTTP/1.1 WebSocket loopback tests --- - public class AbortTest_Invoker_Loopback : AbortTest_Loopback + public sealed class AbortTest_Invoker_Loopback : AbortTest_Loopback { public AbortTest_Invoker_Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseCustomInvoker => true; } - public class AbortTest_HttpClient_Loopback : AbortTest_Loopback + public sealed class AbortTest_HttpClient_Loopback : AbortTest_Loopback { public AbortTest_HttpClient_Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseHttpClient => true; } - public class AbortTest_SharedHandler_Loopback : AbortTest_Loopback + // TODO + public sealed class AbortTest_SharedHandler_Loopback : AbortTest_LoopbackBase //! { public AbortTest_SharedHandler_Loopback(ITestOutputHelper output) : base(output) { } + + [Fact] + public Task Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage() => RunEchoAsync( + RunClient_Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage, useSsl: false); + + [Fact] + public Task Abort_SendAndAbort_Success() => RunEchoAsync( + RunClient_Abort_SendAndAbort_Success, useSsl: false); + + [Fact] + public Task Abort_ReceiveAndAbort_Success() => RunEchoAsync( + RunClient_Abort_ReceiveAndAbort_Success, useSsl: false); + + [Fact] + public Task Abort_CloseAndAbort_Success() => RunEchoAsync( + RunClient_Abort_CloseAndAbort_Success, useSsl: false); + + [Fact] + public Task ClientWebSocket_Abort_CloseOutputAsync() => RunEchoAsync( + RunClient_ClientWebSocket_Abort_CloseOutputAsync, useSsl: false); } // --- HTTP/2 WebSocket loopback tests --- - public class AbortTest_Invoker_Http2 : AbortTest_Invoker_Loopback + public abstract class AbortTest_Http2Loopback : AbortTest_Loopback { - public AbortTest_Invoker_Http2(ITestOutputHelper output) : base(output) { } + public AbortTest_Http2Loopback(ITestOutputHelper output) : base(output) { } internal 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); } - public class AbortTest_HttpClient_Http2 : AbortTest_HttpClient_Loopback + public sealed class AbortTest_Invoker_Http2Loopback : AbortTest_Http2Loopback { - public AbortTest_HttpClient_Http2(ITestOutputHelper output) : base(output) { } - internal 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); + public AbortTest_Invoker_Http2Loopback(ITestOutputHelper output) : base(output) { } + protected override bool UseCustomInvoker => true; + } + + public sealed class AbortTest_HttpClient_Http2Loopback : AbortTest_Http2Loopback + { + public AbortTest_HttpClient_Http2Loopback(ITestOutputHelper output) : base(output) { } + protected override bool UseHttpClient => true; } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs index c69e5f3f3bef34..402f5846647a14 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs @@ -36,15 +36,22 @@ protected async Task RunClient_Abort_ConnectAndAbort_ThrowsWebSocketExceptionWit var ub = new UriBuilder(server); ub.Query = "delay10sec"; + //Console.WriteLine($"Client: Connecting to {ub.Uri}"); + Task t = ConnectAsync(cws, ub.Uri, cts.Token); + + //Console.WriteLine($"Client: aborting websocket"); cws.Abort(); + //Console.WriteLine($"Client: aborted"); WebSocketException ex = await Assert.ThrowsAsync(() => t); + //Console.WriteLine($"Client: ConnectAsync threw exception: {ex}"); Assert.Equal(ResourceHelper.GetExceptionMessage("net_webstatus_ConnectFailure"), ex.Message); Assert.Equal(WebSocketError.Faulted, ex.WebSocketErrorCode); Assert.Equal(WebSocketState.Closed, cws.State); } + //Console.WriteLine($"Client: FINISHED"); } protected async Task RunClient_Abort_SendAndAbort_Success(Uri server) @@ -67,23 +74,29 @@ await TestCancellation(async (cws) => protected async Task RunClient_Abort_ReceiveAndAbort_Success(Uri server) { + //Console.WriteLine($"[Client - {nameof(RunClient_Abort_ReceiveAndAbort_Success)}] Starting test with server: {server}"); await TestCancellation(async (cws) => { var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); + //Console.WriteLine($"[Client - {nameof(RunClient_Abort_ReceiveAndAbort_Success)}] Sending '.delay5sec' message."); await cws.SendAsync( WebSocketData.GetBufferFromText(".delay5sec"), WebSocketMessageType.Text, true, ctsDefault.Token); + //Console.WriteLine($"[Client - {nameof(RunClient_Abort_ReceiveAndAbort_Success)}] Receiving message."); var recvBuffer = new byte[100]; var segment = new ArraySegment(recvBuffer); Task t = cws.ReceiveAsync(segment, ctsDefault.Token); + + //Console.WriteLine($"[Client - {nameof(RunClient_Abort_ReceiveAndAbort_Success)}] Aborting WebSocket."); cws.Abort(); await t; + //Console.WriteLine($"[Client - {nameof(RunClient_Abort_ReceiveAndAbort_Success)}] Test completed."); }, server); } @@ -132,6 +145,8 @@ await cws.SendAsync( } } + // --- External Echo Server "overrides" --- + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] public class AbortTest : AbortTestBase 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..6b052a3356ff76 --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.Loopback.cs @@ -0,0 +1,119 @@ +// 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 Microsoft.DotNet.XUnitExtensions; +using Xunit; +using Xunit.Abstractions; + +namespace System.Net.WebSockets.Client.Tests +{ + // --- Loopback Echo Server "overrides" --- + + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets are not supported on browser")] + public abstract class CancelTest_Loopback : CancelTestBase + { + public CancelTest_Loopback(ITestOutputHelper output) : base(output) { } + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task ConnectAsync_Cancel_ThrowsCancellationException(bool useSsl) => RunEchoAsync( + RunClient_ConnectAsync_Cancel_ThrowsCancellationException, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task SendAsync_Cancel_Success(bool useSsl) => RunEchoAsync( + RunClient_SendAsync_Cancel_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task ReceiveAsync_Cancel_Success(bool useSsl) => RunEchoAsync( + RunClient_ReceiveAsync_Cancel_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task CloseAsync_Cancel_Success(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_Cancel_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task CloseOutputAsync_Cancel_Success(bool useSsl) => RunEchoAsync( + RunClient_CloseOutputAsync_Cancel_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task ReceiveAsync_CancelThenReceive_ThrowsOperationCanceledException(bool useSsl) => RunEchoAsync( + RunClient_ReceiveAsync_CancelThenReceive_ThrowsOperationCanceledException, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException(bool useSsl) => RunEchoAsync( + RunClient_ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException(bool useSsl) => RunEchoAsync( + RunClient_ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException, useSsl); + } + + // --- HTTP/1.1 WebSocket loopback tests --- + + public sealed class CancelTest_Invoker_Loopback : CancelTest_Loopback + { + public CancelTest_Invoker_Loopback(ITestOutputHelper output) : base(output) { } + protected override bool UseCustomInvoker => true; + } + + public sealed class CancelTest_HttpClient_Loopback : CancelTest_Loopback + { + public CancelTest_HttpClient_Loopback(ITestOutputHelper output) : base(output) { } + protected override bool UseHttpClient => true; + } + + // TODO + public sealed class CancelTest_SharedHandler_Loopback : CancelTestBase //! + { + public CancelTest_SharedHandler_Loopback(ITestOutputHelper output) : base(output) { } + + [Fact] + public Task ConnectAsync_Cancel_ThrowsCancellationException() => RunEchoAsync( + RunClient_ConnectAsync_Cancel_ThrowsCancellationException, useSsl: false); + + [Fact] + public Task SendAsync_Cancel_Success() => RunEchoAsync( + RunClient_SendAsync_Cancel_Success, useSsl: false); + + [Fact] + public Task ReceiveAsync_Cancel_Success() => RunEchoAsync( + RunClient_ReceiveAsync_Cancel_Success, useSsl: false); + + [Fact] + public Task CloseAsync_Cancel_Success() => RunEchoAsync( + RunClient_CloseAsync_Cancel_Success, useSsl: false); + + [Fact] + public Task CloseOutputAsync_Cancel_Success() => RunEchoAsync( + RunClient_CloseOutputAsync_Cancel_Success, useSsl: false); + + [Fact] + public Task ReceiveAsync_CancelThenReceive_ThrowsOperationCanceledException() => RunEchoAsync( + RunClient_ReceiveAsync_CancelThenReceive_ThrowsOperationCanceledException, useSsl: false); + + [Fact] + public Task ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException() => RunEchoAsync( + RunClient_ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException, useSsl: false); + + [Fact] + public Task ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException() => RunEchoAsync( + RunClient_ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException, useSsl: false); + } + + // --- HTTP/2 WebSocket loopback tests --- + + public sealed class CancelTest_Invoker_Http2Loopback : CancelTest_Loopback + { + public CancelTest_Invoker_Http2Loopback(ITestOutputHelper output) : base(output) { } + protected override bool UseCustomInvoker => true; + internal override Version HttpVersion => Net.HttpVersion.Version20; + } + + public sealed class CancelTest_HttpClient_Http2Loopback : CancelTest_Loopback + { + public CancelTest_HttpClient_Http2Loopback(ITestOutputHelper output) : base(output) { } + protected override bool UseHttpClient => true; + internal override Version HttpVersion => Net.HttpVersion.Version20; + } +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs index ff6d28c8c40092..e941141b681189 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Linq; using System.Net.Http; +using System.Net.Test.Common; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -29,6 +30,7 @@ public class ClientWebSocketTestBase 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[][] UseSslAndBoolean = ToMemberData(UseSsl_Values, Bool_Values); public static object[][] ToMemberData(IEnumerable data) => data.Select(a => new object[] { a }).ToArray(); @@ -119,17 +121,22 @@ protected static async Task ReceiveEntireMessageAsync(We } } - protected TestConfig _defaultConfig = null!; - protected TestConfig DefaultConfig => _defaultConfig ??= new TestConfig + /*private TestConfig? _config = null!; + protected TestConfig Config => _config ??= new TestConfig { - InvokerType = UseCustomInvoker - ? HttpInvokerType.HttpMessageInvoker - : UseHttpClient - ? HttpInvokerType.HttpClient - : HttpInvokerType.Shared, + InvokerType = InvokerType, ConfigureHttpHandler = ConfigureCustomHandler, HttpVersion = HttpVersion, - }; + };*/ + + /*protected bool? UseSsl { get; set; } + protected Uri? ServerUri { get; set; } + + protected HttpInvokerType InvokerType => UseCustomInvoker + ? HttpInvokerType.HttpMessageInvoker + : UseHttpClient + ? HttpInvokerType.HttpClient + : HttpInvokerType.Shared;*/ protected virtual bool UseCustomInvoker => false; @@ -141,7 +148,31 @@ protected static async Task ReceiveEntireMessageAsync(We internal virtual Version HttpVersion => Net.HttpVersion.Version11; - internal HttpMessageInvoker? GetInvoker() => DefaultConfig.Invoker; + internal HttpMessageInvoker? GetInvoker() + { + if (UseSharedHandler) + { + return null; + } + + var handler = new HttpClientHandler(); + + if (PlatformDetection.IsNotBrowser) + { + handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + } + + ConfigureCustomHandler?.Invoke(handler); + + if (UseCustomInvoker) + { + Debug.Assert(!UseHttpClient); + return new HttpMessageInvoker(handler); + } + + Debug.Assert(UseHttpClient); + return new HttpClient(handler); + } protected Task GetConnectedWebSocket(Uri uri, int timeOutMilliseconds, ITestOutputHelper output) => WebSocketHelper.GetConnectedWebSocket(uri, timeOutMilliseconds, output, o => ConfigureHttpVersion(o, uri), GetInvoker()); @@ -152,6 +183,7 @@ protected Task GetConnectedWebSocket(Uri uri) protected Task ConnectAsync(ClientWebSocket cws, Uri uri, CancellationToken cancellationToken) { ConfigureHttpVersion(cws.Options, uri); + //Console.WriteLine($"Client: starting ConnectAsync"); return cws.ConnectAsync(uri, GetInvoker(), cancellationToken); } @@ -169,6 +201,8 @@ protected void ConfigureHttpVersion(ClientWebSocketOptions options, Uri uri) return; } + //Console.WriteLine($"Client: Original query string = '{uri.Query}'; HttpVersion = {HttpVersion}"); + options.HttpVersion = HttpVersion; options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; @@ -184,11 +218,13 @@ protected void ConfigureHttpVersion(ClientWebSocketOptions options, Uri uri) public static bool WebSocketsSupported { get { return WebSocketHelper.WebSocketsSupported; } } - public record class TestConfig + /*public record class TestConfig { public HttpInvokerType InvokerType { get; set; } public Action? ConfigureHttpHandler { get; set; } - public HttpMessageInvoker? Invoker => CreateInvoker(); + + private HttpMessageInvoker? _invoker = null; + public HttpMessageInvoker? Invoker => _invoker ??= CreateInvoker(); public Version HttpVersion { get; set; } public bool? UseSsl { get; set; } @@ -196,6 +232,11 @@ public record class TestConfig private HttpMessageInvoker? CreateInvoker() { + if (InvokerType == HttpInvokerType.Shared) + { + return null; + } + var handler = new HttpClientHandler(); if (PlatformDetection.IsNotBrowser) @@ -207,7 +248,6 @@ public record class TestConfig return InvokerType switch { - HttpInvokerType.Shared => null, HttpInvokerType.HttpClient => new HttpClient(handler), HttpInvokerType.HttpMessageInvoker => new HttpMessageInvoker(handler), _ => throw new NotImplementedException() @@ -221,6 +261,38 @@ public enum HttpInvokerType Shared = 0, HttpClient, HttpMessageInvoker + }*/ + +#if !TARGET_BROWSER + // Loopback server related functions + protected Task RunEchoAsync(Func clientFunc, bool useSsl) + => LoopbackWebSocketServer.RunEchoAsync(clientFunc, HttpVersion, useSsl, TimeOutMilliseconds); + + protected Task RunEchoHeadersAsync(Func clientFunc, bool useSsl) + { + var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); + var options = new LoopbackWebSocketServer.Options + { + HttpVersion = HttpVersion, + UseSsl = 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); } +#endif } } 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..02e8fcc1ea5e47 --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs @@ -0,0 +1,228 @@ +// 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 Xunit; +using Xunit.Abstractions; + +namespace System.Net.WebSockets.Client.Tests +{ + // --- Loopback Echo Server "overrides" --- + + [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] + [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets are not supported on browser")] + public abstract class CloseTest_Loopback : CloseTestBase + { + public CloseTest_Loopback(ITestOutputHelper output) : base(output) { } + + [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_MemberData))] + public Task CloseAsync_ClientInitiatedClose_Success(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_ClientInitiatedClose_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task CloseAsync_CloseDescriptionIsMaxLength_Success(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_CloseDescriptionIsMaxLength_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task CloseAsync_CloseDescriptionHasUnicode_Success(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_CloseDescriptionHasUnicode_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task CloseAsync_CloseDescriptionIsNull_Success(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_CloseDescriptionIsNull_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task CloseOutputAsync_ExpectedStates(bool useSsl) => RunEchoAsync( + RunClient_CloseOutputAsync_ExpectedStates, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task CloseAsync_CloseOutputAsync_Throws(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_CloseOutputAsync_Throws, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(bool useSsl) => RunEchoAsync( + RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanClose, useSsl); + + [Theory, MemberData(nameof(UseSslAndBoolean))] + public Task CloseOutputAsync_ServerInitiated_CanReceive(bool useSsl, bool delayReceiving) => RunEchoAsync( + server => RunClient_CloseOutputAsync_ServerInitiated_CanReceive(server, delayReceiving), useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task CloseOutputAsync_ServerInitiated_CanSend(bool useSsl) => RunEchoAsync( + RunClient_CloseOutputAsync_ServerInitiated_CanSend, useSsl); + + [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_MemberData))] + public Task CloseOutputAsync_CloseDescriptionIsNull_Success(bool useSsl) => RunEchoAsync( + RunClient_CloseOutputAsync_CloseDescriptionIsNull_Success, useSsl); + + // TODO + /*[ActiveIssue("https://github.com/dotnet/runtime/issues/22000", TargetFrameworkMonikers.Netcoreapp)] + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates(bool useSsl) => RunEchoAsync( + RunClient_CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates, useSsl);*/ + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates(bool useSsl) => RunEchoAsync( + RunClient_CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates, useSsl); + } + + // --- HTTP/1.1 WebSocket loopback tests --- + + public sealed class CloseTest_Invoker_Loopback : CloseTest_Loopback + { + public CloseTest_Invoker_Loopback(ITestOutputHelper output) : base(output) { } + protected override bool UseCustomInvoker => true; + } + + public sealed class CloseTest_HttpClient_Loopback : CloseTest_Loopback + { + public CloseTest_HttpClient_Loopback(ITestOutputHelper output) : base(output) { } + protected override bool UseHttpClient => true; + } + + // TODO + public sealed class CloseTest_SharedHandler_Loopback : CloseTestBase //! + { + public CloseTest_SharedHandler_Loopback(ITestOutputHelper output) : base(output) { } + + // TODO + [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 }); + } + + [Theory, InlineData(false), InlineData(true)] + public Task CloseAsync_ServerInitiatedClose_Success(bool useCloseOutputAsync) => RunEchoAsync( + server => RunClient_CloseAsync_ServerInitiatedClose_Success(server, useCloseOutputAsync), useSsl: false); + + [Fact] + public Task CloseAsync_ClientInitiatedClose_Success() => RunEchoAsync( + RunClient_CloseAsync_ClientInitiatedClose_Success, useSsl: false); + + [Fact] + public Task CloseAsync_CloseDescriptionIsMaxLength_Success() => RunEchoAsync( + RunClient_CloseAsync_CloseDescriptionIsMaxLength_Success, useSsl: false); + + [Fact] + public Task CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException() => RunEchoAsync( + RunClient_CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException, useSsl: false); + + [Fact] + public Task CloseAsync_CloseDescriptionHasUnicode_Success() => RunEchoAsync( + RunClient_CloseAsync_CloseDescriptionHasUnicode_Success, useSsl: false); + + [Fact] + public Task CloseAsync_CloseDescriptionIsNull_Success() => RunEchoAsync( + RunClient_CloseAsync_CloseDescriptionIsNull_Success, useSsl: false); + + [Fact] + public Task CloseOutputAsync_ExpectedStates() => RunEchoAsync( + RunClient_CloseOutputAsync_ExpectedStates, useSsl: false); + + [Fact] + public Task CloseAsync_CloseOutputAsync_Throws() => RunEchoAsync( + RunClient_CloseAsync_CloseOutputAsync_Throws, useSsl: false); + + [Fact] + public Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose() => RunEchoAsync( + RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanClose, useSsl: false); + + [Theory, InlineData(false), InlineData(true)] + public Task CloseOutputAsync_ServerInitiated_CanReceive(bool delayReceiving) => RunEchoAsync( + server => RunClient_CloseOutputAsync_ServerInitiated_CanReceive(server, delayReceiving), useSsl: false); + + [Fact] + public Task CloseOutputAsync_ServerInitiated_CanSend() => RunEchoAsync( + RunClient_CloseOutputAsync_ServerInitiated_CanSend, useSsl: false); + + [Theory, InlineData(false), InlineData(true)] + public Task CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(bool syncState) => RunEchoAsync( + server => RunClient_CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(server, syncState), useSsl: false); + + [Fact] + public Task CloseOutputAsync_CloseDescriptionIsNull_Success() => RunEchoAsync( + RunClient_CloseOutputAsync_CloseDescriptionIsNull_Success, useSsl: false); + + // TODO + /*[ActiveIssue("https://github.com/dotnet/runtime/issues/22000", TargetFrameworkMonikers.Netcoreapp)] + [Fact] + public Task CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates() => RunEchoAsync( + RunClient_CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates, useSsl: false);*/ + + [Fact] + public Task CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates() => RunEchoAsync( + RunClient_CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates, useSsl: false); + } + + // --- HTTP/2 WebSocket loopback tests --- + + public sealed class CloseTest_Invoker_Http2Loopback : CloseTest_Loopback + { + public CloseTest_Invoker_Http2Loopback(ITestOutputHelper output) : base(output) { } + protected override bool UseCustomInvoker => true; + internal override Version HttpVersion => Net.HttpVersion.Version20; + } + + public sealed class CloseTest_HttpClient_Http2Loopback : CloseTest_Loopback + { + public CloseTest_HttpClient_Http2Loopback(ITestOutputHelper output) : base(output) { } + protected override bool UseHttpClient => true; + internal override Version HttpVersion => Net.HttpVersion.Version20; + } +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs index 42c795f879bef0..4b0515de4e43f7 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs @@ -438,53 +438,6 @@ protected async Task RunClient_CloseAsync_DuringConcurrentReceiveAsync_ExpectedS } } } - - [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); - - 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 }); - } } [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] 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..970948ee829e0e --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs @@ -0,0 +1,231 @@ +// 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.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_Loopback : ConnectTestBase + { + public ConnectTest_Loopback(ITestOutputHelper output) : base(output) { } + + // --- Loopback Echo Server "overrides" --- + + + + //[ActiveIssue("https://github.com/dotnet/runtime/issues/1895")] + //[Theory, MemberData(nameof(UnavailableWebSocketServers))] + //public Task ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(Uri server, string exceptionMessage, WebSocketError errorCode) + // => RunClient_ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(server, exceptionMessage, errorCode); + + /*[ConditionalTheory, MemberData(nameof(UseSsl_MemberData))] + public Task EchoBinaryMessage_Success(bool useSsl) => RunEchoAsync( + RunClient_EchoBinaryMessage_Success, useSsl); + + [ConditionalTheory, MemberData(nameof(UseSsl_MemberData))] + public Task EchoTextMessage_Success(bool useSsl) => RunEchoAsync( + RunClient_EchoTextMessage_Success, useSsl); + + [SkipOnPlatform(TestPlatforms.Browser, "SetRequestHeader not supported on browser")] + [ConditionalTheory, MemberData(nameof(UseSsl_MemberData))] + public Task ConnectAsync_AddCustomHeaders_Success(bool useSsl) => RunEchoHeadersAsync( + RunClient_ConnectAsync_AddCustomHeaders_Success, useSsl); + + [SkipOnPlatform(TestPlatforms.Browser, "Cookies not supported on browser")] + [ConditionalTheory, MemberData(nameof(UseSsl_MemberData))] + public Task ConnectAsync_CookieHeaders_Success(bool useSsl) => RunEchoHeadersAsync( + RunClient_ConnectAsync_CookieHeaders_Success, useSsl); + + [ActiveIssue("https://github.com/dotnet/runtime/issues/101115", typeof(PlatformDetection), nameof(PlatformDetection.IsFirefox))] + [ConditionalTheory, MemberData(nameof(UseSsl_MemberData))] + public Task ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException(bool useSsl) => RunEchoAsync( + RunClient_ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException, useSsl); + + [ConditionalTheory, MemberData(nameof(UseSsl_MemberData))] + public Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol(bool useSsl) => RunEchoAsync( + RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol, useSsl); + + [SkipOnPlatform(TestPlatforms.Browser, "Proxy not supported on Browser")] + [ConditionalTheory, MemberData(nameof(UseSsl_MemberData))] + public Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(bool useSsl) => RunEchoAsync( + RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState, useSsl);*/ + } + + // --- HTTP/1.1 WebSocket loopback tests --- + + /*public class ConnectTest_Invoker_Loopback : ConnectTest_Loopback + { + public ConnectTest_Invoker_Loopback(ITestOutputHelper output) : base(output) { } + protected override bool UseCustomInvoker => true; + }*/ + + /*public class ConnectTest_HttpClient_Loopback : ConnectTest_Loopback + { + public ConnectTest_HttpClient_Loopback(ITestOutputHelper output) : base(output) { } + protected override bool UseHttpClient => true; + }*/ + + public class ConnectTest_SharedHandler_Loopback : ConnectTest_Loopback + { + public ConnectTest_SharedHandler_Loopback(ITestOutputHelper output) : base(output) { } + + // TODO + [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 }); + } + } + + // --- HTTP/2 WebSocket loopback tests --- + + /*public class ConnectTest_Invoker_Http2 : ConnectTest_Loopback + { + public ConnectTest_Invoker_Http2(ITestOutputHelper output) : base(output) { } + protected override bool UseCustomInvoker => true; + internal override Version HttpVersion => Net.HttpVersion.Version20; + }*/ + + /*public class ConnectTest_HttpClient_Http2 : ConnectTest_Loopback + { + public ConnectTest_HttpClient_Http2(ITestOutputHelper output) : base(output) { } + protected override bool UseHttpClient => true; + internal override Version HttpVersion => Net.HttpVersion.Version20; + }*/ +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs index 1a0a30868292ff..999da088c92321 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs @@ -169,29 +169,6 @@ protected async Task RunClient_ConnectAsync_AddCustomHeaders_Success(Uri server) } } - [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 }); - } - protected async Task RunClient_ConnectAsync_CookieHeaders_Success(Uri server) { using (var cws = new ClientWebSocket()) @@ -304,24 +281,6 @@ protected async Task RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClose } } - [ConditionalFact(nameof(WebSocketsSupported))] - [SkipOnPlatform(TestPlatforms.Browser, "SetRequestHeader not supported on Browser")] - 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 }); - } - [ConditionalFact(nameof(WebSocketsSupported))] public async Task ConnectAsync_CancellationRequestedBeforeConnect_ThrowsOperationCanceledException() { @@ -345,103 +304,6 @@ public async Task ConnectAsync_CancellationRequestedInflightConnect_ThrowsOperat await Assert.ThrowsAnyAsync(() => t); } } - - [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 => - { - 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 }); - } - - [ConditionalFact(nameof(WebSocketsSupported))] - [SkipOnPlatform(TestPlatforms.Browser, "CollectHttpResponseDetails not supported on Browser")] - 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 }); - } - - [ConditionalFact(nameof(WebSocketsSupported))] - [SkipOnPlatform(TestPlatforms.Browser, "CollectHttpResponseDetails not supported on Browser")] - 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 }); - } - - [ConditionalFact(nameof(WebSocketsSupported))] - [SkipOnPlatform(TestPlatforms.Browser, "CollectHttpResponseDetails not supported on Browser")] - 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 }); - } } [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] 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 477768986c709c..68c6e0d4e54ed8 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs @@ -1,7 +1,9 @@ // 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.Http; +using System.Net.Sockets; using System.Net.Test.Common; using System.Threading; using System.Threading.Tasks; @@ -40,49 +42,157 @@ public static Task RunAsync( return RunAsyncPrivate(loopbackClientFunc, loopbackServerFunc, options, cancellationToken); } - public static Task RunEchoAsync(Func loopbackClientFunc, Options options, CancellationToken cancellationToken) + public static Task RunEchoAsync(Func loopbackClientFunc, Version httpVersion, bool useSsl, int timeOutMilliseconds) { - Assert.False(options.DisposeClientWebSocket, "Not supported in this overload"); - Assert.False(options.DisposeHttpInvoker, "Not supported in this overload"); - Assert.Null(options.HttpInvoker); // Not supported in this overload + var timeoutCts = new CancellationTokenSource(timeOutMilliseconds); + var options = new Options + { + HttpVersion = httpVersion, + UseSsl = useSsl, + IgnoreServerErrors = true, + AbortServerOnClientExit = true, + ParseEchoOptions = true + }; - return RunAsyncPrivate(loopbackClientFunc, (data, token) => RunEchoServerAsync(data, options, token), options, cancellationToken); + //Console.WriteLine($"[{nameof(RunEchoAsync)}] Starting Echo test"); + + return RunAsyncPrivate( + loopbackClientFunc, + (data, token) => RunEchoServerAsync(data, options, token), + options, + timeoutCts.Token); } private static Task RunAsyncPrivate( Func loopbackClientFunc, Func loopbackServerFunc, Options options, - CancellationToken cancellationToken) + CancellationToken globalCt) { - if (options.HttpVersion == HttpVersion.Version11) + Func clientFunc; + CancellationToken cancellationToken; + CancellationToken clientExitCt; + + if (options.AbortServerOnClientExit) { - return LoopbackServer.CreateClientAndServerAsync( - loopbackClientFunc, - async server => + //Console.WriteLine($"[{nameof(RunAsyncPrivate)}] AbortServerOnClientExit=true"); + CancellationTokenSource clientExitCts = new CancellationTokenSource(); + clientExitCt = clientExitCts.Token; + cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(globalCt, clientExitCt).Token; + + clientFunc = async uri => + { + //Console.WriteLine($"[Client - {nameof(clientFunc)}] clientExitCt canceled={clientExitCt.IsCancellationRequested}, cancellationToken canceled={cancellationToken.IsCancellationRequested}"); + try + { + //Console.WriteLine($"[Client - {nameof(clientFunc)}] Starting client"); + await loopbackClientFunc(uri).ConfigureAwait(false); + //Console.WriteLine($"[Client - {nameof(clientFunc)}] Client completed SUCCESSFULLY"); + } + //catch (Exception ex) + //{ + //Console.WriteLine($"[Client - {nameof(clientFunc)}] Client FAILED with exception: {ex.Message}"); + // throw; + //} + finally + { + clientExitCts.Cancel(); + //Console.WriteLine($"[Client - {nameof(clientFunc)}] clientExitCts cancelled (clientExitCt canceled={clientExitCt.IsCancellationRequested}, cancellationToken canceled={cancellationToken.IsCancellationRequested})"); + } + }; + } + else + { + //Console.WriteLine($"[{nameof(RunAsyncPrivate)}] AbortServerOnClientExit=false"); + clientFunc = loopbackClientFunc; + cancellationToken = globalCt; + clientExitCt = CancellationToken.None; + } + + async Task RunHttpServer( + TServer server, + Func, Options, CancellationToken, Task> serverFunc + ) where TServer : GenericLoopbackServer + { + try + { + //Console.WriteLine($"[Server - {nameof(RunHttpServer)}<{typeof(TServer).Name}>] Starting server"); + await Task.Run(() => + serverFunc(server, loopbackServerFunc, options, cancellationToken), + cancellationToken); + //Console.WriteLine($"[Server - {nameof(RunHttpServer)}<{typeof(TServer).Name}>] Server completed SUCCESSFULLY"); + } + catch (OperationCanceledException) when (options.AbortServerOnClientExit && clientExitCt.IsCancellationRequested) { } // expected + catch (WebSocketException we) when (options.AbortServerOnClientExit && we.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { } // expected + catch (SocketException se) when (options.AbortServerOnClientExit && se.SocketErrorCode == SocketError.ConnectionReset) { } // expected + catch (IOException ie) when (options.AbortServerOnClientExit && ie.InnerException is SocketException se && se.SocketErrorCode == SocketError.ConnectionReset) { } // expected + catch (SocketException) when (options.IgnoreServerErrors) { } // ignore + catch (IOException) when (options.IgnoreServerErrors) { } // ignore + catch (WebSocketException we) when (options.IgnoreServerErrors) + { + if (we.WebSocketErrorCode == WebSocketError.InvalidState) { - await server.AcceptConnectionAsync(async connection => + const string closeOnClosedMsg = "The WebSocket is in an invalid state ('Closed') for this operation. Valid states are: 'Open, CloseSent, CloseReceived'"; + const string closeOnAbortedMsg = "The WebSocket is in an invalid state ('Aborted') for this operation. Valid states are: 'Open, CloseSent, CloseReceived'"; + if (we.Message == closeOnClosedMsg || we.Message == closeOnAbortedMsg) + { + return; // ignore + } + } + + Console.WriteLine($"[Server - {nameof(RunHttpServer)}<{typeof(TServer).Name}>] Abort on an ignored WebSocketException ({we.WebSocketErrorCode}): {we.Message}"); + } + catch (Exception e) when (options.IgnoreServerErrors) + { + Console.WriteLine($"[Server - {nameof(RunHttpServer)}<{typeof(TServer).Name}>] Abort on an ignored exception: {e}"); + } + } + + if (options.HttpVersion == HttpVersion.Version11) + { + //Console.WriteLine($"[Server - {nameof(RunHttp11Server)}] Waiting for client connection..."); + + static Task RunHttp11Server(LoopbackServer server, Func loopbackServerFunc, Options options, CancellationToken cancellationToken) + => server.AcceptConnectionAsync(async connection => { - var requestData = await WebSocketHandshakeHelper.ProcessHttp11RequestAsync(connection, options.SkipServerHandshakeResponse, cancellationToken).ConfigureAwait(false); + //Console.WriteLine($"[Server - {nameof(RunHttp11Server)}] Processing HTTP/1.1 request..."); + var requestData = await WebSocketHandshakeHelper.ProcessHttp11RequestAsync( + connection, + options.SkipServerHandshakeResponse, + options.ParseEchoOptions, + cancellationToken).ConfigureAwait(false); + //Console.WriteLine($"[Server - {nameof(RunHttp11Server)}] WebSocketRequestData: {requestData}"); await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); + //Console.WriteLine($"[Server - {nameof(RunHttp11Server)}] loopbackServerFunc completed"); }); - }, + + return LoopbackServer.CreateClientAndServerAsync( + clientFunc, + server => RunHttpServer(server, RunHttp11Server), new LoopbackServer.Options { WebSocketEndpoint = true, UseSsl = options.UseSsl }); } else if (options.HttpVersion == HttpVersion.Version20) { - return Http2LoopbackServer.CreateClientAndServerAsync( - loopbackClientFunc, - async server => - { - var requestData = await WebSocketHandshakeHelper.ProcessHttp2RequestAsync(server, options.SkipServerHandshakeResponse, cancellationToken).ConfigureAwait(false); - var http2Connection = requestData.Http2Connection!; - var http2StreamId = requestData.Http2StreamId.Value; - - await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); + //Console.WriteLine($"[Server - {nameof(RunHttp2Server)}] Waiting for client connection..."); + static async Task RunHttp2Server(Http2LoopbackServer server, Func loopbackServerFunc, Options options, CancellationToken cancellationToken) + { + var requestData = await WebSocketHandshakeHelper.ProcessHttp2RequestAsync( + server, + options.SkipServerHandshakeResponse, + options.ParseEchoOptions, + cancellationToken).ConfigureAwait(false); + //Console.WriteLine($"[Server - {nameof(RunHttp2Server)}] WebSocketRequestData: {requestData}"); + await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); + //Console.WriteLine($"[Server - {nameof(RunHttp2Server)}] loopbackServerFunc completed"); + //Console.WriteLine($"[Server - {nameof(RunHttp2Server)}] Shutting down HTTP/2 connection..."); + await requestData.Http2Connection!.ShutdownIgnoringErrorsAsync( + requestData.Http2StreamId.Value); + //Console.WriteLine($"[Server - {nameof(RunHttp2Server)}] HTTP/2 connection shutdown completed"); + }; - await http2Connection.ShutdownIgnoringErrorsAsync(http2StreamId).ConfigureAwait(false); - }, + return Http2LoopbackServer.CreateClientAndServerAsync( + clientFunc, + server => RunHttpServer(server, RunHttp2Server), new Http2Options { WebSocketEndpoint = true, UseSsl = options.UseSsl }); } else @@ -97,64 +207,52 @@ private static async Task RunServerAsync( Options options, CancellationToken cancellationToken) { - WebSocket serverWebSocket = null!; - CancellationTokenRegistration registration = default; - try - { - var wsOptions = new WebSocketCreationOptions { IsServer = true }; - options.ConfigureServerOptions?.Invoke(wsOptions); + var wsOptions = new WebSocketCreationOptions { IsServer = true }; + options.ConfigureServerOptions?.Invoke(wsOptions); - serverWebSocket = WebSocket.CreateFromStream(requestData.TransportStream, wsOptions); - registration = cancellationToken.Register(() => serverWebSocket.Abort()); + var serverWebSocket = WebSocket.CreateFromStream(requestData.TransportStream, wsOptions); + //Console.WriteLine($"[Server - {nameof(RunServerAsync)}] Created server websocket"); + using var registration = cancellationToken.Register(serverWebSocket.Abort); - await serverWebSocketFunc(serverWebSocket, cancellationToken).ConfigureAwait(false); - } - catch (Exception) when (options.IgnoreServerErrors) - { - // ignore - } - finally + //Console.WriteLine($"[Server - {nameof(RunServerAsync)}] Processing..."); + await serverWebSocketFunc(serverWebSocket, cancellationToken).ConfigureAwait(false); + //Console.WriteLine($"[Server - {nameof(RunServerAsync)}] Completed"); + + if (options.DisposeServerWebSocket) { - registration.Dispose(); - if (options.DisposeServerWebSocket) - { - serverWebSocket?.Dispose(); - } + serverWebSocket?.Dispose(); } } - private static async Task RunEchoServerAsync(WebSocketRequestData data, Options options, CancellationToken cancellationToken) + private static Task RunEchoServerAsync(WebSocketRequestData data, Options options, CancellationToken cancellationToken) { - try - { - WebSocketEchoOptions echoOptions = await WebSocketEchoHelper.ProcessOptions(data.Query, cancellationToken); + Assert.NotNull(data.EchoOptions); + WebSocketEchoOptions echoOptions = data.EchoOptions.Value; - if (options.ConfigureServerOptions is not null && echoOptions.SubProtocol is not null) + if (echoOptions.SubProtocol is not null) + { + Options original = options; + Action originalConfigure = original.ConfigureServerOptions; + Action combinedConfigure = o => { - Options copy = options; - options = copy with { - ConfigureServerOptions = o => - { - o.SubProtocol = echoOptions.SubProtocol; - copy.ConfigureServerOptions.Invoke(o); - } - }; - } + o.SubProtocol = echoOptions.SubProtocol; + originalConfigure?.Invoke(o); + }; - await RunServerAsync( - data, - (serverWebSocket, token) => WebSocketEchoHelper.RunEchoAll( - serverWebSocket, - echoOptions.ReplyWithPartialMessages, - echoOptions.ReplyWithEnhancedCloseMessage, - token), - options, - cancellationToken); - } - catch (Exception) when (options.IgnoreServerErrors) - { - // ignore + options = original with { ConfigureServerOptions = combinedConfigure }; } + + //Console.WriteLine($"[Server - {nameof(RunEchoServerAsync)}] Starting Echo server"); + + return RunServerAsync( + data, + (serverWebSocket, token) => WebSocketEchoHelper.RunEchoAll( + serverWebSocket, + echoOptions.ReplyWithPartialMessages, + echoOptions.ReplyWithEnhancedCloseMessage, + token), + options, + cancellationToken); } private static async Task RunClientAsync( @@ -204,7 +302,9 @@ public record class Options() 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 Action? ConfigureServerOptions { get; set; } public bool DisposeClientWebSocket { 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 66142458a1d05e..3275a5b7bccf1d 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketHandshakeHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketHandshakeHelper.cs @@ -13,9 +13,15 @@ namespace System.Net.WebSockets.Client.Tests { public static class WebSocketHandshakeHelper { - public static async Task ProcessHttp11RequestAsync(LoopbackServer.Connection connection, bool skipServerHandshakeResponse = false, CancellationToken cancellationToken = default) + public static async Task ProcessHttp11RequestAsync( + LoopbackServer.Connection connection, + bool skipServerHandshakeResponse = false, + bool parseEchoOptions = false, + CancellationToken cancellationToken = default) { + //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] Handling HTTP/1.1 request on {connection.Socket.LocalEndPoint}..."); List headers = await connection.ReadRequestHeaderAsync().WaitAsync(cancellationToken).ConfigureAwait(false); + //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] Received headers: {string.Join(", ", headers)}"); var data = new WebSocketRequestData() { @@ -23,13 +29,28 @@ public static async Task ProcessHttp11RequestAsync(Loopbac Http11Connection = connection }; - // 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) + if (parseEchoOptions) { - int spaceIndex = headers[0].IndexOf(' ', queryIndex); - data.Query = headers[0].Substring(queryIndex, spaceIndex - queryIndex); + // 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); + //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] query: {query}"); + + // NOTE: ProcessOptions needs to be called before sending the server response + // because it may be configured to delay the response. + + //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] Processing options..."); + data.EchoOptions = await WebSocketEchoHelper.ProcessOptions(query).ConfigureAwait(false); + //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] Parsed options: {data.EchoOptions}"); + } + else + { + data.EchoOptions = WebSocketEchoOptions.Default; + } } for (int i = 1; i < headers.Count; ++i) @@ -48,7 +69,9 @@ public static async Task ProcessHttp11RequestAsync(Loopbac if (!skipServerHandshakeResponse) { + //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] Sending server handshake response..."); await SendHttp11ServerResponseAsync(connection, secWebSocketKey, cancellationToken).ConfigureAwait(false); + //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] Sent server handshake response."); } data.TransportStream = connection.Stream; @@ -61,7 +84,11 @@ private static async Task SendHttp11ServerResponseAsync(LoopbackServer.Connectio await connection.WriteStringAsync(serverResponse).WaitAsync(cancellationToken).ConfigureAwait(false); } - public static async Task ProcessHttp2RequestAsync(Http2LoopbackServer server, bool skipServerHandshakeResponse = false, 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); @@ -86,11 +113,23 @@ public static async Task ProcessHttp2RequestAsync(Http2Loo var isValidOpeningHandshake = httpRequestData.Method == HttpMethod.Connect.ToString() && data.Headers.ContainsKey(":protocol"); Assert.True(isValidOpeningHandshake); - // HTTP/2 CONNECT requests drop path and query from the request URI, - // see https://datatracker.ietf.org/doc/html/rfc7540#section-8.3: - // > The ":scheme" and ":path" pseudo-header fields MUST be omitted. - // The original query string was passed in a custom header. - data.Query = data.Headers.GetValueOrDefault(WebSocketHelper.OriginalQueryStringHeader); + if (parseEchoOptions) + { + // HTTP/2 CONNECT requests drop path and query from the request URI, + // see https://datatracker.ietf.org/doc/html/rfc7540#section-8.3: + // > The ":scheme" and ":path" pseudo-header fields MUST be omitted. + // The original query string was passed 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).ConfigureAwait(false); + } + else + { + data.EchoOptions = WebSocketEchoOptions.Default; + } + } if (!skipServerHandshakeResponse) { 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 fbd838361d53d0..aabb675db88cad 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketRequestData.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketRequestData.cs @@ -10,7 +10,7 @@ namespace System.Net.WebSockets.Client.Tests public class WebSocketRequestData { public Dictionary Headers { get; set; } = new Dictionary(); - public string? Query { get; set; } + public WebSocketEchoOptions? EchoOptions { get; set; } public Stream? TransportStream { get; set; } 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 0841f63376e2ac..77da7e0c46791d 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 @@ -52,12 +52,15 @@ + + + From af6e50175e9a1e5af471a180a97be186ce1b1616 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Fri, 28 Feb 2025 02:40:56 +0000 Subject: [PATCH 05/20] Add thread-safety option to Http2LoopbackServer --- .../Net/Http/Http2LoopbackConnection.cs | 56 ++++++++++++++++++- .../System/Net/Http/Http2LoopbackServer.cs | 1 + .../tests/KeepAliveTest.Loopback.cs | 3 +- .../LoopbackServer/LoopbackWebSocketServer.cs | 6 +- .../System.Net.WebSockets.Client.Tests.csproj | 3 + 5 files changed, 64 insertions(+), 5 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs index e607c42aa48ba8..b23d7c389659a0 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,60 @@ 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 = 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).ConfigureAwait(false); + } + finally + { + _readLock.Release(); + } + }, + writeAsyncFunc: async (buffer, offset, count, cancellationToken) => + { + await _writeLock.WaitAsync(cancellationToken); + try + { + await stream.WriteAsync(buffer, offset, count, cancellationToken); + } + finally + { + _writeLock.Release(); + } + }, + flushAsyncFunc: async (cancellationToken) => + { + await _writeLock.WaitAsync(cancellationToken); + try + { + await stream.FlushAsync(cancellationToken).ConfigureAwait(false); + } + finally + { + _writeLock.Release(); + } + }, + disposeFunc: (disposing) => stream.Dispose() + ); + } } public override string ToString() @@ -83,7 +133,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..39c8210bb68bcd 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() { 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 8d948c5fb3a7f6..7239e8188c62f3 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs @@ -38,7 +38,8 @@ public Task KeepAlive_LongDelayBetweenSendReceives_Succeeds(bool useSsl) { clientOptions.KeepAliveInterval = TimeSpan.FromMilliseconds(100); clientOptions.KeepAliveTimeout = TimeSpan.FromSeconds(1); - } + }, + ConfigureHttp2Options = http2Options => http2Options.EnsureThreadSafeIO = true }; return LoopbackWebSocketServer.RunAsync( 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 68c6e0d4e54ed8..69a215947ab715 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs @@ -190,10 +190,13 @@ static async Task RunHttp2Server(Http2LoopbackServer server, Func RunHttpServer(server, RunHttp2Server), - new Http2Options { WebSocketEndpoint = true, UseSsl = options.UseSsl }); + http2Options); } else { @@ -306,6 +309,7 @@ public record class Options() public bool IgnoreServerErrors { get; set; } public bool AbortServerOnClientExit { get; set; } public Action? ConfigureServerOptions { get; set; } + public Action? ConfigureHttp2Options { get; set; } public bool DisposeClientWebSocket { get; set; } public bool DisposeHttpInvoker { get; set; } 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 77da7e0c46791d..a74ab0c15defdf 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 @@ -49,6 +49,9 @@ + + + From 3bfe66a23714e754331b89d15f7f355617ae19f7 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Fri, 28 Feb 2025 17:40:19 +0000 Subject: [PATCH 06/20] Test fixes, add more tests --- .../tests/ClientWebSocketTestBase.cs | 13 ++- .../tests/ConnectTest.Loopback.cs | 88 ++++++++++++++----- .../tests/ConnectTest.cs | 29 ++++-- .../LoopbackServer/LoopbackWebSocketServer.cs | 24 ++--- .../WebSocketHandshakeHelper.cs | 3 + .../tests/SendReceiveTest.cs | 2 +- .../tests/WebSocketHelper.cs | 4 + 7 files changed, 113 insertions(+), 50 deletions(-) diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs index e941141b681189..c49dcd50fcccb2 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs @@ -159,7 +159,7 @@ protected static async Task ReceiveEntireMessageAsync(We if (PlatformDetection.IsNotBrowser) { - handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; + handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; } ConfigureCustomHandler?.Invoke(handler); @@ -183,7 +183,6 @@ protected Task GetConnectedWebSocket(Uri uri) protected Task ConnectAsync(ClientWebSocket cws, Uri uri, CancellationToken cancellationToken) { ConfigureHttpVersion(cws.Options, uri); - //Console.WriteLine($"Client: starting ConnectAsync"); return cws.ConnectAsync(uri, GetInvoker(), cancellationToken); } @@ -201,11 +200,14 @@ protected void ConfigureHttpVersion(ClientWebSocketOptions options, Uri uri) return; } - //Console.WriteLine($"Client: Original query string = '{uri.Query}'; HttpVersion = {HttpVersion}"); - options.HttpVersion = HttpVersion; options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; + if (UseSharedHandler && uri.Scheme == "wss") + { + options.RemoteCertificateValidationCallback = (_, _, _, _) => true; + } + if (HttpVersion == Net.HttpVersion.Version20 && uri.Query is not null or "" or "?") { // HTTP/2 CONNECT requests drop path and query from the request URI, @@ -283,12 +285,15 @@ protected Task RunEchoHeadersAsync(Func clientFunc, bool useSsl) clientFunc, async (requestData, token) => { + // Console.WriteLine($"[Server - {nameof(RunEchoHeadersAsync)}] WebSocket.CreateFromStream"); var serverWebSocket = WebSocket.CreateFromStream( requestData.TransportStream, new WebSocketCreationOptions { IsServer = true }); using var registration = token.Register(serverWebSocket.Abort); + // Console.WriteLine($"[Server - {nameof(RunEchoHeadersAsync)}] RunEchoHeaders"); await WebSocketEchoHelper.RunEchoHeaders(serverWebSocket, requestData.Headers, token); + // Console.WriteLine($"[Server - {nameof(RunEchoHeadersAsync)}] RunEchoHeaders completed"); }, options, timeoutCts.Token); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs index 970948ee829e0e..171777a5a59246 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs @@ -20,61 +20,68 @@ public ConnectTest_Loopback(ITestOutputHelper output) : base(output) { } // --- Loopback Echo Server "overrides" --- - - //[ActiveIssue("https://github.com/dotnet/runtime/issues/1895")] //[Theory, MemberData(nameof(UnavailableWebSocketServers))] //public Task ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(Uri server, string exceptionMessage, WebSocketError errorCode) // => RunClient_ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(server, exceptionMessage, errorCode); - /*[ConditionalTheory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl_MemberData))] public Task EchoBinaryMessage_Success(bool useSsl) => RunEchoAsync( RunClient_EchoBinaryMessage_Success, useSsl); - [ConditionalTheory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl_MemberData))] public Task EchoTextMessage_Success(bool useSsl) => RunEchoAsync( RunClient_EchoTextMessage_Success, useSsl); - [SkipOnPlatform(TestPlatforms.Browser, "SetRequestHeader not supported on browser")] - [ConditionalTheory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl_MemberData))] public Task ConnectAsync_AddCustomHeaders_Success(bool useSsl) => RunEchoHeadersAsync( RunClient_ConnectAsync_AddCustomHeaders_Success, useSsl); - [SkipOnPlatform(TestPlatforms.Browser, "Cookies not supported on browser")] - [ConditionalTheory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl_MemberData))] public Task ConnectAsync_CookieHeaders_Success(bool useSsl) => RunEchoHeadersAsync( RunClient_ConnectAsync_CookieHeaders_Success, useSsl); - [ActiveIssue("https://github.com/dotnet/runtime/issues/101115", typeof(PlatformDetection), nameof(PlatformDetection.IsFirefox))] - [ConditionalTheory, MemberData(nameof(UseSsl_MemberData))] + /*[Theory, MemberData(nameof(UseSsl_MemberData))] public Task ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException(bool useSsl) => RunEchoAsync( RunClient_ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException, useSsl); - [ConditionalTheory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl_MemberData))] public Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol(bool useSsl) => RunEchoAsync( - RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol, useSsl); + RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol, useSsl);*/ - [SkipOnPlatform(TestPlatforms.Browser, "Proxy not supported on Browser")] - [ConditionalTheory, MemberData(nameof(UseSsl_MemberData))] - public Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(bool useSsl) => RunEchoAsync( - RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState, useSsl);*/ + // TODO: this test is HTTP/1.1 only + //[Theory, MemberData(nameof(UseSsl_MemberData))] + //public Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(bool useSsl) => RunEchoAsync( + // RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState, useSsl); } // --- HTTP/1.1 WebSocket loopback tests --- - /*public class ConnectTest_Invoker_Loopback : ConnectTest_Loopback + // TODO + public abstract class ConnectTest_Loopback_Http11Only : ConnectTest_Loopback + { + public ConnectTest_Loopback_Http11Only(ITestOutputHelper output) : base(output) { } + + // TODO: this test is HTTP/1.1 only + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(bool useSsl) => RunEchoAsync( + RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState, useSsl); + } + + public sealed class ConnectTest_Invoker_Loopback : ConnectTest_Loopback_Http11Only //! { public ConnectTest_Invoker_Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseCustomInvoker => true; - }*/ + } - /*public class ConnectTest_HttpClient_Loopback : ConnectTest_Loopback + public sealed class ConnectTest_HttpClient_Loopback : ConnectTest_Loopback_Http11Only //! { public ConnectTest_HttpClient_Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseHttpClient => true; - }*/ + } - public class ConnectTest_SharedHandler_Loopback : ConnectTest_Loopback + // TODO + public sealed class ConnectTest_SharedHandler_Loopback : ConnectTestBase //! { public ConnectTest_SharedHandler_Loopback(ITestOutputHelper output) : base(output) { } @@ -211,18 +218,51 @@ await LoopbackServer.CreateClientAndServerAsync(async uri => Assert.Equal(expectedHost, host); }), new LoopbackServer.Options { WebSocketEndpoint = true }); } + + //[ActiveIssue("https://github.com/dotnet/runtime/issues/1895")] + //[Theory, MemberData(nameof(UnavailableWebSocketServers))] + //public Task ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(Uri server, string exceptionMessage, WebSocketError errorCode) + // => RunClient_ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(server, exceptionMessage, errorCode); + + [Fact] + public Task EchoBinaryMessage_Success() => RunEchoAsync( + RunClient_EchoBinaryMessage_Success, useSsl: false); + + [Fact] + public Task EchoTextMessage_Success() => RunEchoAsync( + RunClient_EchoTextMessage_Success, useSsl: false); + + [Fact] + public Task ConnectAsync_AddCustomHeaders_Success() => RunEchoHeadersAsync( + RunClient_ConnectAsync_AddCustomHeaders_Success, useSsl: false); + + [Fact] + public Task ConnectAsync_CookieHeaders_Success() => RunEchoHeadersAsync( + RunClient_ConnectAsync_CookieHeaders_Success, useSsl: false); + + /*[Fact] + public Task ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException() => RunEchoAsync( + RunClient_ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException, useSsl: false); + + [Fact] + public Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol() => RunEchoAsync( + RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol, useSsl: false);*/ + + [Fact] + public Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState() => RunEchoAsync( + RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState, useSsl: false); } // --- HTTP/2 WebSocket loopback tests --- - /*public class ConnectTest_Invoker_Http2 : ConnectTest_Loopback + public sealed class ConnectTest_Invoker_Http2 : ConnectTest_Loopback { public ConnectTest_Invoker_Http2(ITestOutputHelper output) : base(output) { } protected override bool UseCustomInvoker => true; internal override Version HttpVersion => Net.HttpVersion.Version20; - }*/ + } - /*public class ConnectTest_HttpClient_Http2 : ConnectTest_Loopback + /*public sealed class ConnectTest_HttpClient_Http2 : ConnectTest_Loopback { public ConnectTest_HttpClient_Http2(ITestOutputHelper output) : base(output) { } protected override bool UseHttpClient => true; diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs index 999da088c92321..4ca5399bfc9184 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs @@ -162,8 +162,8 @@ protected async Task RunClient_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); + 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); } @@ -174,20 +174,29 @@ 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 }; - cws.Options.Cookies.Add(server, cookie1); - cws.Options.Cookies.Add(server, cookie2); - cws.Options.Cookies.Add(server, secureCookie); + cookies.Add(server, cookie1); + cookies.Add(server, cookie2); + 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 || @@ -208,6 +217,8 @@ protected async Task RunClient_ConnectAsync_CookieHeaders_Success(Uri server) Assert.Equal(WebSocketMessageType.Text, recvResult.MessageType); string headers = WebSocketData.GetTextFromBuffer(new ArraySegment(buffer, 0, recvResult.Count)); + // Console.WriteLine(headers); + Assert.Contains("Cookies=Are Yummy", headers); Assert.Contains("Especially=Chocolate Chip", headers); Assert.Equal(server.Scheme == "wss", headers.Contains("Occasionally=Raisin")); 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 69a215947ab715..3e9adf011c0659 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs @@ -85,9 +85,9 @@ private static Task RunAsyncPrivate( //Console.WriteLine($"[Client - {nameof(clientFunc)}] clientExitCt canceled={clientExitCt.IsCancellationRequested}, cancellationToken canceled={cancellationToken.IsCancellationRequested}"); try { - //Console.WriteLine($"[Client - {nameof(clientFunc)}] Starting client"); + // Console.WriteLine($"[Client - {nameof(clientFunc)}] Starting client"); await loopbackClientFunc(uri).ConfigureAwait(false); - //Console.WriteLine($"[Client - {nameof(clientFunc)}] Client completed SUCCESSFULLY"); + // Console.WriteLine($"[Client - {nameof(clientFunc)}] Client completed SUCCESSFULLY"); } //catch (Exception ex) //{ @@ -116,11 +116,11 @@ Func, Options, Canc { try { - //Console.WriteLine($"[Server - {nameof(RunHttpServer)}<{typeof(TServer).Name}>] Starting server"); + // Console.WriteLine($"[Server - {nameof(RunHttpServer)}<{typeof(TServer).Name}>] Starting server"); await Task.Run(() => serverFunc(server, loopbackServerFunc, options, cancellationToken), cancellationToken); - //Console.WriteLine($"[Server - {nameof(RunHttpServer)}<{typeof(TServer).Name}>] Server completed SUCCESSFULLY"); + // Console.WriteLine($"[Server - {nameof(RunHttpServer)}<{typeof(TServer).Name}>] Server completed SUCCESSFULLY"); } catch (OperationCanceledException) when (options.AbortServerOnClientExit && clientExitCt.IsCancellationRequested) { } // expected catch (WebSocketException we) when (options.AbortServerOnClientExit && we.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { } // expected @@ -173,7 +173,7 @@ static Task RunHttp11Server(LoopbackServer server, Func loopbackServerFunc, Options options, CancellationToken cancellationToken) { var requestData = await WebSocketHandshakeHelper.ProcessHttp2RequestAsync( @@ -181,13 +181,13 @@ static async Task RunHttp2Server(Http2LoopbackServer server, Func ProcessHttp2RequestAsync( Http2StreamId = streamId }; + //Console.WriteLine($"[Server - {nameof(ProcessHttp2RequestAsync)}] Headers:"); + foreach (var header in httpRequestData.Headers) { Assert.NotNull(header.Name); data.Headers.Add(header.Name, header.Value); + //Console.WriteLine($"[Server - {nameof(ProcessHttp2RequestAsync)}] {header.Name}: {data.Headers[header.Name] ?? ""}"); } var isValidOpeningHandshake = httpRequestData.Method == HttpMethod.Connect.ToString() && data.Headers.ContainsKey(":protocol"); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs index 426dfb261ccc57..0e72694174d6e8 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs @@ -123,7 +123,7 @@ protected async Task RunClient_SendReceive_PartialMessageBeforeCompleteMessageAr var ub = new UriBuilder(server); ub.Query = "replyWithPartialMessages"; - using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket(ub.Uri, TimeOutMilliseconds, _output)) + using (ClientWebSocket cws = await GetConnectedWebSocket(ub.Uri)) { var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs index 14bdecf397387d..a4805926ab9b27 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs @@ -101,6 +101,10 @@ public static Task GetConnectedWebSocket( { var cws = new ClientWebSocket(); configureOptions(cws.Options); + if (PlatformDetection.IsNotBrowser && invoker == null && server.Scheme == "wss") + { + cws.Options.RemoteCertificateValidationCallback ??= (_, _, _, _) => true; + } using (var cts = new CancellationTokenSource(timeOutMilliseconds)) { From fbc5090881c9c9e1886758f436f581ae3153356f Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Sat, 1 Mar 2025 00:17:19 +0000 Subject: [PATCH 07/20] Refactor LoopbackWebSocketServer, fix subprotocol tests --- .../tests/ClientWebSocketTestBase.cs | 12 +- .../tests/ConnectTest.Loopback.cs | 9 +- .../tests/ConnectTest.cs | 7 +- .../tests/LoopbackHelper.cs | 3 +- .../LoopbackWebSocketServer.Echo.cs | 57 +++++ .../LoopbackWebSocketServer.Http.cs | 161 +++++++++++++ .../LoopbackServer/LoopbackWebSocketServer.cs | 214 ++---------------- .../WebSocketHandshakeHelper.cs | 65 +++++- .../LoopbackServer/WebSocketRequestData.cs | 2 +- .../System.Net.WebSockets.Client.Tests.csproj | 2 + 10 files changed, 313 insertions(+), 219 deletions(-) create mode 100644 src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Echo.cs create mode 100644 src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs index c49dcd50fcccb2..157619cd7d8662 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs @@ -208,12 +208,14 @@ protected void ConfigureHttpVersion(ClientWebSocketOptions options, Uri uri) options.RemoteCertificateValidationCallback = (_, _, _, _) => true; } - if (HttpVersion == Net.HttpVersion.Version20 && uri.Query is not null or "" or "?") + if (HttpVersion == Net.HttpVersion.Version20 && uri.Query is not (null or "" or "?")) { - // HTTP/2 CONNECT requests drop path and query from the request URI, - // see https://datatracker.ietf.org/doc/html/rfc7540#section-8.3: - // > The ":scheme" and ":path" pseudo-header fields MUST be omitted. - // Saving the original query string in a custom header. + // 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. + options.SetRequestHeader(WebSocketHelper.OriginalQueryStringHeader, uri.Query); } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs index 171777a5a59246..e0cddb6fc05698 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Net.Test.Common; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Microsoft.DotNet.XUnitExtensions; @@ -41,13 +42,13 @@ public Task ConnectAsync_AddCustomHeaders_Success(bool useSsl) => RunEchoHeaders public Task ConnectAsync_CookieHeaders_Success(bool useSsl) => RunEchoHeadersAsync( RunClient_ConnectAsync_CookieHeaders_Success, useSsl); - /*[Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl_MemberData))] public Task ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException(bool useSsl) => RunEchoAsync( RunClient_ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException, useSsl); [Theory, MemberData(nameof(UseSsl_MemberData))] public Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol(bool useSsl) => RunEchoAsync( - RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol, useSsl);*/ + RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol, useSsl); // TODO: this test is HTTP/1.1 only //[Theory, MemberData(nameof(UseSsl_MemberData))] @@ -242,11 +243,11 @@ public Task ConnectAsync_CookieHeaders_Success() => RunEchoHeadersAsync( /*[Fact] public Task ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException() => RunEchoAsync( - RunClient_ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException, useSsl: false); + RunClient_ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException, useSsl: false);*/ [Fact] public Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol() => RunEchoAsync( - RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol, useSsl: false);*/ + RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol, useSsl: false); [Fact] public Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState() => RunEchoAsync( diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs index 4ca5399bfc9184..5d50cb2979d54e 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs @@ -13,7 +13,7 @@ namespace System.Net.WebSockets.Client.Tests { - public sealed class InvokerConnectTest : ConnectTestBase + public sealed class InvokerConnectTest : ConnectTest { public InvokerConnectTest(ITestOutputHelper output) : base(output) { } @@ -90,7 +90,7 @@ public async Task ConnectAsync_CustomInvokerWithIncompatibleWebSocketOptions_Thr } } - public sealed class HttpClientConnectTest : ConnectTestBase + public sealed class HttpClientConnectTest : ConnectTest { public HttpClientConnectTest(ITestOutputHelper output) : base(output) { } @@ -241,7 +241,8 @@ protected async Task RunClient_ConnectAsync_PassNoSubProtocol_ServerRequires_Thr 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 || // TODO + ex.WebSocketErrorCode == WebSocketError.Faulted || ex.WebSocketErrorCode == WebSocketError.NotAWebSocket, $"Actual WebSocketErrorCode {ex.WebSocketErrorCode} {ex.InnerException?.Message} \n {ex}"); Assert.Equal(WebSocketState.Closed, cws.State); } 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..d119c5d766ec7c --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Echo.cs @@ -0,0 +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.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, Version httpVersion, bool useSsl, int timeOutMilliseconds) + { + var timeoutCts = new CancellationTokenSource(timeOutMilliseconds); + var options = new Options + { + HttpVersion = httpVersion, + UseSsl = useSsl, + SkipServerHandshakeResponse = true, // to negotiate subprotocols and extensions + IgnoreServerErrors = true, + AbortServerOnClientExit = true, + ParseEchoOptions = true + }; + + return RunAsyncPrivate( + loopbackClientFunc, + (data, token) => RunEchoServerWebSocketAsync(data, options, token), + options, + timeoutCts.Token); + } + + private static async Task RunEchoServerWebSocketAsync(WebSocketRequestData data, Options options, CancellationToken cancellationToken) + { + Assert.NotNull(data.EchoOptions); + WebSocketEchoOptions echoOptions = data.EchoOptions.Value; + + if (echoOptions.SubProtocol is not null) + { + Assert.Null(options.ServerSubProtocol); + options = options with { ServerSubProtocol = echoOptions.SubProtocol }; + } + + 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..47378dad401f48 --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs @@ -0,0 +1,161 @@ +// 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 + { + private static Task RunClientAndServerAsync( + Func clientFunc, + Func loopbackServerFunc, + Options options, + CancellationToken clientExitCt, + CancellationToken globalCt) + { + if (options.HttpVersion == HttpVersion.Version11) + { + return LoopbackServer.CreateClientAndServerAsync( + clientFunc, + server => RunHttpServer( + ProcessHttp11WebSocketRequest, server, loopbackServerFunc, options, clientExitCt, globalCt), + new LoopbackServer.Options { WebSocketEndpoint = true, UseSsl = options.UseSsl }); + } + + if (options.HttpVersion == HttpVersion.Version20) + { + var http2Options = new Http2Options { WebSocketEndpoint = true, UseSsl = options.UseSsl }; + options.ConfigureHttp2Options?.Invoke(http2Options); + + return Http2LoopbackServer.CreateClientAndServerAsync( + clientFunc, + server => RunHttpServer( + ProcessHttp2WebSocketRequest, server, loopbackServerFunc, options, clientExitCt, globalCt), + http2Options); + } + + throw new ArgumentException(nameof(options.HttpVersion)); + } + + 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); + + await requestData.Http2Connection!.ShutdownIgnoringErrorsAsync(requestData.Http2StreamId.Value); + } + + private static async Task RunHttpServer( + Func, Options, CancellationToken, Task> httpServerFunc, + THttpServer httpServer, + Func wsServerFunc, + Options options, + CancellationToken clientExitCt, + CancellationToken globalCt) + where THttpServer : GenericLoopbackServer + { + try + { + using CancellationTokenSource linkedCts = + CancellationTokenSource.CreateLinkedTokenSource(globalCt, clientExitCt); + + await httpServerFunc(httpServer, wsServerFunc, options, linkedCts.Token) + .WaitAsync(linkedCts.Token).ConfigureAwait(false); + } + catch (Exception e) when (options.IgnoreServerErrors) + { + if (e is OperationCanceledException && clientExitCt.IsCancellationRequested) + { + return; // expected for aborting on client exit + } + + if (e is WebSocketException we) + { + if (we.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) + { + return; // expected for aborting on client exit + } + + if (we.WebSocketErrorCode == WebSocketError.InvalidState) + { + const string closeOnClosedMsg = "The WebSocket is in an invalid state ('Closed') for this operation. Valid states are: 'Open, CloseSent, CloseReceived'"; + const string closeOnAbortedMsg = "The WebSocket is in an invalid state ('Aborted') for this operation. Valid states are: 'Open, CloseSent, CloseReceived'"; + if (we.Message == closeOnClosedMsg || we.Message == closeOnAbortedMsg) + { + return; // expected, see https://github.com/dotnet/runtime/issues/22000 + } + } + + Console.WriteLine($"[WARN] Server aborted on a WebSocketException ({we.WebSocketErrorCode}): {we.Message}"); + return; // ignore + } + + if (e is IOException or SocketException) + { + return; // ignore + } + + throw; // don't swallow Assert failures and unexpected exceptions + } + } + + 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 3e9adf011c0659..fcebf665b20ae4 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs @@ -1,9 +1,7 @@ // 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.Http; -using System.Net.Sockets; using System.Net.Test.Common; using System.Threading; using System.Threading.Tasks; @@ -11,7 +9,7 @@ namespace System.Net.WebSockets.Client.Tests { - public static class LoopbackWebSocketServer + public static partial class LoopbackWebSocketServer { public static Task RunAsync( Func clientWebSocketFunc, @@ -22,8 +20,8 @@ public static Task RunAsync( Assert.False(options.SkipServerHandshakeResponse, "Not supported in this overload"); return RunAsyncPrivate( - uri => RunClientAsync(uri, clientWebSocketFunc, options, cancellationToken), - (requestData, token) => RunServerAsync(requestData, serverWebSocketFunc, options, token), + uri => RunClientWebSocketAsync(uri, clientWebSocketFunc, options, cancellationToken), + (requestData, token) => RunServerWebSocketAsync(requestData, serverWebSocketFunc, options, token), options, cancellationToken); } @@ -42,184 +40,44 @@ public static Task RunAsync( return RunAsyncPrivate(loopbackClientFunc, loopbackServerFunc, options, cancellationToken); } - public static Task RunEchoAsync(Func loopbackClientFunc, Version httpVersion, bool useSsl, int timeOutMilliseconds) - { - var timeoutCts = new CancellationTokenSource(timeOutMilliseconds); - var options = new Options - { - HttpVersion = httpVersion, - UseSsl = useSsl, - IgnoreServerErrors = true, - AbortServerOnClientExit = true, - ParseEchoOptions = true - }; - - //Console.WriteLine($"[{nameof(RunEchoAsync)}] Starting Echo test"); - - return RunAsyncPrivate( - loopbackClientFunc, - (data, token) => RunEchoServerAsync(data, options, token), - options, - timeoutCts.Token); - } - private static Task RunAsyncPrivate( Func loopbackClientFunc, Func loopbackServerFunc, Options options, CancellationToken globalCt) { - Func clientFunc; - CancellationToken cancellationToken; - CancellationToken clientExitCt; - - if (options.AbortServerOnClientExit) + if (!options.AbortServerOnClientExit) { - //Console.WriteLine($"[{nameof(RunAsyncPrivate)}] AbortServerOnClientExit=true"); - CancellationTokenSource clientExitCts = new CancellationTokenSource(); - clientExitCt = clientExitCts.Token; - cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(globalCt, clientExitCt).Token; - - clientFunc = async uri => - { - //Console.WriteLine($"[Client - {nameof(clientFunc)}] clientExitCt canceled={clientExitCt.IsCancellationRequested}, cancellationToken canceled={cancellationToken.IsCancellationRequested}"); - try - { - // Console.WriteLine($"[Client - {nameof(clientFunc)}] Starting client"); - await loopbackClientFunc(uri).ConfigureAwait(false); - // Console.WriteLine($"[Client - {nameof(clientFunc)}] Client completed SUCCESSFULLY"); - } - //catch (Exception ex) - //{ - //Console.WriteLine($"[Client - {nameof(clientFunc)}] Client FAILED with exception: {ex.Message}"); - // throw; - //} - finally - { - clientExitCts.Cancel(); - //Console.WriteLine($"[Client - {nameof(clientFunc)}] clientExitCts cancelled (clientExitCt canceled={clientExitCt.IsCancellationRequested}, cancellationToken canceled={cancellationToken.IsCancellationRequested})"); - } - }; + return RunClientAndServerAsync( + loopbackClientFunc, loopbackServerFunc, options, CancellationToken.None, globalCt); } - else - { - //Console.WriteLine($"[{nameof(RunAsyncPrivate)}] AbortServerOnClientExit=false"); - clientFunc = loopbackClientFunc; - cancellationToken = globalCt; - clientExitCt = CancellationToken.None; - } - - async Task RunHttpServer( - TServer server, - Func, Options, CancellationToken, Task> serverFunc - ) where TServer : GenericLoopbackServer - { - try - { - // Console.WriteLine($"[Server - {nameof(RunHttpServer)}<{typeof(TServer).Name}>] Starting server"); - await Task.Run(() => - serverFunc(server, loopbackServerFunc, options, cancellationToken), - cancellationToken); - // Console.WriteLine($"[Server - {nameof(RunHttpServer)}<{typeof(TServer).Name}>] Server completed SUCCESSFULLY"); - } - catch (OperationCanceledException) when (options.AbortServerOnClientExit && clientExitCt.IsCancellationRequested) { } // expected - catch (WebSocketException we) when (options.AbortServerOnClientExit && we.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) { } // expected - catch (SocketException se) when (options.AbortServerOnClientExit && se.SocketErrorCode == SocketError.ConnectionReset) { } // expected - catch (IOException ie) when (options.AbortServerOnClientExit && ie.InnerException is SocketException se && se.SocketErrorCode == SocketError.ConnectionReset) { } // expected - catch (SocketException) when (options.IgnoreServerErrors) { } // ignore - catch (IOException) when (options.IgnoreServerErrors) { } // ignore - catch (WebSocketException we) when (options.IgnoreServerErrors) - { - if (we.WebSocketErrorCode == WebSocketError.InvalidState) - { - const string closeOnClosedMsg = "The WebSocket is in an invalid state ('Closed') for this operation. Valid states are: 'Open, CloseSent, CloseReceived'"; - const string closeOnAbortedMsg = "The WebSocket is in an invalid state ('Aborted') for this operation. Valid states are: 'Open, CloseSent, CloseReceived'"; - if (we.Message == closeOnClosedMsg || we.Message == closeOnAbortedMsg) - { - return; // ignore - } - } - - Console.WriteLine($"[Server - {nameof(RunHttpServer)}<{typeof(TServer).Name}>] Abort on an ignored WebSocketException ({we.WebSocketErrorCode}): {we.Message}"); - } - catch (Exception e) when (options.IgnoreServerErrors) - { - Console.WriteLine($"[Server - {nameof(RunHttpServer)}<{typeof(TServer).Name}>] Abort on an ignored exception: {e}"); - } - } - - if (options.HttpVersion == HttpVersion.Version11) - { - //Console.WriteLine($"[Server - {nameof(RunHttp11Server)}] Waiting for client connection..."); - static Task RunHttp11Server(LoopbackServer server, Func loopbackServerFunc, Options options, CancellationToken cancellationToken) - => server.AcceptConnectionAsync(async connection => - { - //Console.WriteLine($"[Server - {nameof(RunHttp11Server)}] Processing HTTP/1.1 request..."); - var requestData = await WebSocketHandshakeHelper.ProcessHttp11RequestAsync( - connection, - options.SkipServerHandshakeResponse, - options.ParseEchoOptions, - cancellationToken).ConfigureAwait(false); - //Console.WriteLine($"[Server - {nameof(RunHttp11Server)}] WebSocketRequestData: {requestData}"); - await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); - //Console.WriteLine($"[Server - {nameof(RunHttp11Server)}] loopbackServerFunc completed"); - }); + CancellationTokenSource clientExitCts = new CancellationTokenSource(); - return LoopbackServer.CreateClientAndServerAsync( - clientFunc, - server => RunHttpServer(server, RunHttp11Server), - new LoopbackServer.Options { WebSocketEndpoint = true, UseSsl = options.UseSsl }); - } - else if (options.HttpVersion == HttpVersion.Version20) - { - // Console.WriteLine($"[Server - {nameof(RunHttp2Server)}] Waiting for client connection..."); - static async Task RunHttp2Server(Http2LoopbackServer server, Func loopbackServerFunc, Options options, CancellationToken cancellationToken) + return RunClientAndServerAsync( + async uri => { - var requestData = await WebSocketHandshakeHelper.ProcessHttp2RequestAsync( - server, - options.SkipServerHandshakeResponse, - options.ParseEchoOptions, - cancellationToken).ConfigureAwait(false); - // Console.WriteLine($"[Server - {nameof(RunHttp2Server)}] WebSocketRequestData: {requestData}"); - await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); - // Console.WriteLine($"[Server - {nameof(RunHttp2Server)}] loopbackServerFunc completed"); - // Console.WriteLine($"[Server - {nameof(RunHttp2Server)}] Shutting down HTTP/2 connection..."); - await requestData.Http2Connection!.ShutdownIgnoringErrorsAsync( - requestData.Http2StreamId.Value); - // Console.WriteLine($"[Server - {nameof(RunHttp2Server)}] HTTP/2 connection shutdown completed"); - }; - - var http2Options = new Http2Options { WebSocketEndpoint = true, UseSsl = options.UseSsl }; - options.ConfigureHttp2Options?.Invoke(http2Options); - - return Http2LoopbackServer.CreateClientAndServerAsync( - clientFunc, - server => RunHttpServer(server, RunHttp2Server), - http2Options); - } - else - { - throw new ArgumentException(nameof(options.HttpVersion)); - } + await loopbackClientFunc(uri); + clientExitCts.Cancel(); + }, + loopbackServerFunc, + options, + clientExitCts.Token, + globalCt); } - private static async Task RunServerAsync( + private static async Task RunServerWebSocketAsync( WebSocketRequestData requestData, Func serverWebSocketFunc, Options options, CancellationToken cancellationToken) { - var wsOptions = new WebSocketCreationOptions { IsServer = true }; - options.ConfigureServerOptions?.Invoke(wsOptions); + var wsOptions = new WebSocketCreationOptions { IsServer = true, SubProtocol = options.ServerSubProtocol }; var serverWebSocket = WebSocket.CreateFromStream(requestData.TransportStream, wsOptions); - // Console.WriteLine($"[Server - {nameof(RunServerAsync)}] Created server websocket"); using var registration = cancellationToken.Register(serverWebSocket.Abort); - // Console.WriteLine($"[Server - {nameof(RunServerAsync)}] Processing..."); await serverWebSocketFunc(serverWebSocket, cancellationToken).ConfigureAwait(false); - // Console.WriteLine($"[Server - {nameof(RunServerAsync)}] Completed"); if (options.DisposeServerWebSocket) { @@ -227,38 +85,7 @@ private static async Task RunServerAsync( } } - private static Task RunEchoServerAsync(WebSocketRequestData data, Options options, CancellationToken cancellationToken) - { - Assert.NotNull(data.EchoOptions); - WebSocketEchoOptions echoOptions = data.EchoOptions.Value; - - if (echoOptions.SubProtocol is not null) - { - Options original = options; - Action originalConfigure = original.ConfigureServerOptions; - Action combinedConfigure = o => - { - o.SubProtocol = echoOptions.SubProtocol; - originalConfigure?.Invoke(o); - }; - - options = original with { ConfigureServerOptions = combinedConfigure }; - } - - //Console.WriteLine($"[Server - {nameof(RunEchoServerAsync)}] Starting Echo server"); - - return RunServerAsync( - data, - (serverWebSocket, token) => WebSocketEchoHelper.RunEchoAll( - serverWebSocket, - echoOptions.ReplyWithPartialMessages, - echoOptions.ReplyWithEnhancedCloseMessage, - token), - options, - cancellationToken); - } - - private static async Task RunClientAsync( + private static async Task RunClientWebSocketAsync( Uri uri, Func clientWebSocketFunc, Options options, @@ -308,7 +135,8 @@ public record class Options() public bool ParseEchoOptions { get; set; } public bool IgnoreServerErrors { get; set; } public bool AbortServerOnClientExit { get; set; } - public Action? ConfigureServerOptions { get; set; } + public string? ServerSubProtocol { get; set; } + public string? ServerExtensions { get; set; } public Action? ConfigureHttp2Options { get; set; } public bool DisposeClientWebSocket { 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 32229e4b2065cc..19bb632e4d70bf 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketHandshakeHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketHandshakeHelper.cs @@ -2,9 +2,11 @@ // 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; +using System.Text; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -69,8 +71,16 @@ public static async Task ProcessHttp11RequestAsync( 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"); + } + } + //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] Sending server handshake response..."); - await SendHttp11ServerResponseAsync(connection, secWebSocketKey, cancellationToken).ConfigureAwait(false); + await SendHttp11ServerResponseAsync(connection, secWebSocketKey, cancellationToken: cancellationToken).ConfigureAwait(false); //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] Sent server handshake response."); } @@ -78,9 +88,14 @@ public static async Task ProcessHttp11RequestAsync( 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); } @@ -118,10 +133,12 @@ public static async Task ProcessHttp2RequestAsync( if (parseEchoOptions) { - // HTTP/2 CONNECT requests drop path and query from the request URI, - // see https://datatracker.ietf.org/doc/html/rfc7540#section-8.3: - // > The ":scheme" and ":path" pseudo-header fields MUST be omitted. - // The original query string was passed in a custom header. + // 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 @@ -136,6 +153,14 @@ public static async Task ProcessHttp2RequestAsync( 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); } @@ -143,12 +168,28 @@ public static async Task ProcessHttp2RequestAsync( 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) @@ -156,7 +197,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) { @@ -174,7 +215,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 aabb675db88cad..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,7 +9,7 @@ namespace System.Net.WebSockets.Client.Tests { public class WebSocketRequestData { - public Dictionary Headers { get; set; } = new Dictionary(); + public Dictionary Headers { get; set; } = new Dictionary(StringComparer.OrdinalIgnoreCase); public WebSocketEchoOptions? EchoOptions { get; set; } public Stream? TransportStream { get; set; } 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 a74ab0c15defdf..24746286df9a34 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 @@ -69,6 +69,8 @@ + + From c1e65451534958047be5a9267dfca7d0ac9bfee3 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Thu, 27 Mar 2025 11:00:48 +0000 Subject: [PATCH 08/20] wip --- .../tests/SendReceiveTest.Loopback.cs | 91 +++++++++++++++++++ .../tests/SendReceiveTest.cs | 4 +- 2 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs 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..16f1b2ba2d82a4 --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs @@ -0,0 +1,91 @@ +// 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_Loopback : SendReceiveTestBase + { + public SendReceiveTest(ITestOutputHelper output) : base(output) { } + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(Uri server) + => RunClient_SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(server); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task SendReceive_PartialMessageBeforeCompleteMessageArrives_Success(Uri server) + => RunClient_SendReceive_PartialMessageBeforeCompleteMessageArrives_Success(server); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage(Uri server) + => RunClient_SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage(server); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task SendAsync_MultipleOutstandingSendOperations_Throws(Uri server) + => RunClient_SendAsync_MultipleOutstandingSendOperations_Throws(server); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(Uri server) + => RunClient_ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(server); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(Uri server) + => RunClient_SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(server); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task SendReceive_VaryingLengthBuffers_Success(Uri server) + => RunClient_SendReceive_VaryingLengthBuffers_Success(server); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task SendReceive_Concurrent_Success(Uri server) + => RunClient_SendReceive_Concurrent_Success(server); + + //[Fact] + //public Task SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated() + // => RunClient_SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated(); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task ZeroByteReceive_CompletesWhenDataAvailable(Uri server) + => RunClient_ZeroByteReceive_CompletesWhenDataAvailable(server); + } + + public class MemorySendReceiveTest : SendReceiveTest + { + public MemorySendReceiveTest(ITestOutputHelper output) : base(output) { } + + protected override async Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) + { + ValueWebSocketReceiveResult r = await ws.ReceiveAsync( + (Memory)arraySegment, + cancellationToken).ConfigureAwait(false); + return new WebSocketReceiveResult(r.Count, r.MessageType, r.EndOfMessage, ws.CloseStatus, ws.CloseStatusDescription); + } + + protected override Task SendAsync(WebSocket ws, ArraySegment arraySegment, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) => + ws.SendAsync( + (ReadOnlyMemory)arraySegment, + messageType, + endOfMessage, + cancellationToken).AsTask(); + } + + public class ArraySegmentSendReceiveTest : SendReceiveTest + { + public ArraySegmentSendReceiveTest(ITestOutputHelper output) : base(output) { } + + protected override Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) => + ws.ReceiveAsync(arraySegment, cancellationToken); + + protected override Task SendAsync(WebSocket ws, ArraySegment arraySegment, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) => + ws.SendAsync(arraySegment, messageType, endOfMessage, cancellationToken); + } +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs index 0e72694174d6e8..7f2404eb932d8b 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs @@ -546,10 +546,10 @@ public Task SendReceive_VaryingLengthBuffers_Success(Uri server) public Task SendReceive_Concurrent_Success(Uri server) => RunClient_SendReceive_Concurrent_Success(server); - [ActiveIssue("https://github.com/dotnet/runtime/issues/54153", TestPlatforms.Browser)] + /*[ActiveIssue("https://github.com/dotnet/runtime/issues/54153", TestPlatforms.Browser)] [Fact] public Task SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated() - => RunClient_SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated(); + => RunClient_SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated();*/ [Theory, MemberData(nameof(EchoServers))] public Task ZeroByteReceive_CompletesWhenDataAvailable(Uri server) From c4843937ff1c039978b0b7ae9ec2a67b10b2b676 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Thu, 26 Jun 2025 23:44:56 +0100 Subject: [PATCH 09/20] Fixes --- .../tests/AbortTest.cs | 11 ---------- .../tests/CloseTest.cs | 2 +- .../tests/ConnectTest.Http2.cs | 4 ++-- .../LoopbackWebSocketServer.Http.cs | 13 +++++++++++- .../LoopbackServer/LoopbackWebSocketServer.cs | 20 ++++++++++++------- .../WebSocketHandshakeHelper.cs | 10 ---------- .../tests/SendReceiveTest.Loopback.cs | 6 +++--- 7 files changed, 31 insertions(+), 35 deletions(-) diff --git a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs index 402f5846647a14..c07faf2188d73e 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs @@ -36,22 +36,16 @@ protected async Task RunClient_Abort_ConnectAndAbort_ThrowsWebSocketExceptionWit var ub = new UriBuilder(server); ub.Query = "delay10sec"; - //Console.WriteLine($"Client: Connecting to {ub.Uri}"); - Task t = ConnectAsync(cws, ub.Uri, cts.Token); - //Console.WriteLine($"Client: aborting websocket"); cws.Abort(); - //Console.WriteLine($"Client: aborted"); WebSocketException ex = await Assert.ThrowsAsync(() => t); - //Console.WriteLine($"Client: ConnectAsync threw exception: {ex}"); Assert.Equal(ResourceHelper.GetExceptionMessage("net_webstatus_ConnectFailure"), ex.Message); Assert.Equal(WebSocketError.Faulted, ex.WebSocketErrorCode); Assert.Equal(WebSocketState.Closed, cws.State); } - //Console.WriteLine($"Client: FINISHED"); } protected async Task RunClient_Abort_SendAndAbort_Success(Uri server) @@ -74,29 +68,24 @@ await TestCancellation(async (cws) => protected async Task RunClient_Abort_ReceiveAndAbort_Success(Uri server) { - //Console.WriteLine($"[Client - {nameof(RunClient_Abort_ReceiveAndAbort_Success)}] Starting test with server: {server}"); await TestCancellation(async (cws) => { var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); - //Console.WriteLine($"[Client - {nameof(RunClient_Abort_ReceiveAndAbort_Success)}] Sending '.delay5sec' message."); await cws.SendAsync( WebSocketData.GetBufferFromText(".delay5sec"), WebSocketMessageType.Text, true, ctsDefault.Token); - //Console.WriteLine($"[Client - {nameof(RunClient_Abort_ReceiveAndAbort_Success)}] Receiving message."); var recvBuffer = new byte[100]; var segment = new ArraySegment(recvBuffer); Task t = cws.ReceiveAsync(segment, ctsDefault.Token); - //Console.WriteLine($"[Client - {nameof(RunClient_Abort_ReceiveAndAbort_Success)}] Aborting WebSocket."); cws.Abort(); await t; - //Console.WriteLine($"[Client - {nameof(RunClient_Abort_ReceiveAndAbort_Success)}] Test completed."); }, server); } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs index 9f52cefa0d0bdc..556501eaf7d821 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs @@ -545,7 +545,7 @@ await RemoteExecutor.Invoke(static (typeName) => 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); + }, new LoopbackWebSocketServer.Options{ HttpVersion = Net.HttpVersion.Version11, UseSsl = true, HttpInvoker = test.GetInvoker() }, timeoutCts.Token); }, GetType().FullName).DisposeAsync(); } } 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 f12e4f4487ba69..3b033b9cbb2324 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs @@ -237,7 +237,7 @@ public async Task ConnectAsync_Http11WithRequestVersionOrHigher_DowngradeSuccess using (var cws = new ClientWebSocket()) using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) { - cws.Options.HttpVersion = HttpVersion.Version11; + cws.Options.HttpVersion = Net.HttpVersion.Version11; cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; await cws.ConnectAsync(server, GetInvoker(), cts.Token); Assert.Equal(WebSocketState.Open, cws.State); @@ -253,7 +253,7 @@ await LoopbackServer.CreateServerAsync(async (server, url) => using (var cws = new ClientWebSocket()) using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) { - cws.Options.HttpVersion = HttpVersion.Version11; + cws.Options.HttpVersion = Net.HttpVersion.Version11; cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; Task connectTask = cws.ConnectAsync(url, GetInvoker(), cts.Token); 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 index 47378dad401f48..0fd2c87cda241f 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs @@ -74,7 +74,18 @@ private static async Task ProcessHttp2WebSocketRequest( await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); - await requestData.Http2Connection!.ShutdownIgnoringErrorsAsync(requestData.Http2StreamId.Value); + DateTime now = DateTime.UtcNow; + + if (options.AbortServerOnClientExit) + { + // Wait for the client to exit + await requestData.Http2Connection!.WaitForConnectionShutdownAsync(ignoreUnexpectedFrames: true).ConfigureAwait(false); + } + else + { + // This will send GOAWAY + await requestData.Http2Connection!.ShutdownIgnoringErrorsAsync(requestData.Http2StreamId.Value).ConfigureAwait(false); + } } private static async Task RunHttpServer( 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 fcebf665b20ae4..e066133296e58f 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs @@ -17,7 +17,7 @@ public static Task RunAsync( Options options, CancellationToken cancellationToken) { - Assert.False(options.SkipServerHandshakeResponse, "Not supported in this overload"); + Assert.False(options.SkipServerHandshakeResponse, "Required to create ClientWebSocket"); return RunAsyncPrivate( uri => RunClientWebSocketAsync(uri, clientWebSocketFunc, options, cancellationToken), @@ -32,9 +32,9 @@ public static Task RunAsync( 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.False(options.DisposeClientWebSocket, "ClientWebSocket is not created in this overload"); + Assert.False(options.DisposeServerWebSocket, "ServerWebSocket is not created in this overload"); + Assert.False(options.DisposeHttpInvoker, "HttpInvoker is not used in this overload"); Assert.Null(options.HttpInvoker); // Not supported in this overload return RunAsyncPrivate(loopbackClientFunc, loopbackServerFunc, options, cancellationToken); @@ -57,8 +57,14 @@ private static Task RunAsyncPrivate( return RunClientAndServerAsync( async uri => { - await loopbackClientFunc(uri); - clientExitCts.Cancel(); + try + { + await loopbackClientFunc(uri); + } + finally + { + clientExitCts.Cancel(); + } }, loopbackServerFunc, options, @@ -81,7 +87,7 @@ private static async Task RunServerWebSocketAsync( if (options.DisposeServerWebSocket) { - serverWebSocket?.Dispose(); + serverWebSocket.Dispose(); } } 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 19bb632e4d70bf..f869535e3f2b08 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketHandshakeHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketHandshakeHelper.cs @@ -21,9 +21,7 @@ public static async Task ProcessHttp11RequestAsync( bool parseEchoOptions = false, CancellationToken cancellationToken = default) { - //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] Handling HTTP/1.1 request on {connection.Socket.LocalEndPoint}..."); List headers = await connection.ReadRequestHeaderAsync().WaitAsync(cancellationToken).ConfigureAwait(false); - //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] Received headers: {string.Join(", ", headers)}"); var data = new WebSocketRequestData() { @@ -40,14 +38,11 @@ public static async Task ProcessHttp11RequestAsync( { int spaceIndex = headers[0].IndexOf(' ', queryIndex); string query = headers[0].Substring(queryIndex, spaceIndex - queryIndex); - //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] query: {query}"); // NOTE: ProcessOptions needs to be called before sending the server response // because it may be configured to delay the response. - //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] Processing options..."); data.EchoOptions = await WebSocketEchoHelper.ProcessOptions(query).ConfigureAwait(false); - //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] Parsed options: {data.EchoOptions}"); } else { @@ -79,9 +74,7 @@ public static async Task ProcessHttp11RequestAsync( } } - //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] Sending server handshake response..."); await SendHttp11ServerResponseAsync(connection, secWebSocketKey, cancellationToken: cancellationToken).ConfigureAwait(false); - //Console.WriteLine($"[Server - {nameof(ProcessHttp11RequestAsync)}] Sent server handshake response."); } data.TransportStream = connection.Stream; @@ -119,13 +112,10 @@ public static async Task ProcessHttp2RequestAsync( Http2StreamId = streamId }; - //Console.WriteLine($"[Server - {nameof(ProcessHttp2RequestAsync)}] Headers:"); - foreach (var header in httpRequestData.Headers) { Assert.NotNull(header.Name); data.Headers.Add(header.Name, header.Value); - //Console.WriteLine($"[Server - {nameof(ProcessHttp2RequestAsync)}] {header.Name}: {data.Headers[header.Name] ?? ""}"); } var isValidOpeningHandshake = httpRequestData.Method == HttpMethod.Connect.ToString() && data.Headers.ContainsKey(":protocol"); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs index 16f1b2ba2d82a4..d81975be8e80eb 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs @@ -49,9 +49,9 @@ public Task SendReceive_VaryingLengthBuffers_Success(Uri server) public Task SendReceive_Concurrent_Success(Uri server) => RunClient_SendReceive_Concurrent_Success(server); - //[Fact] - //public Task SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated() - // => RunClient_SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated(); + [Fact] + public Task SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated() + => RunClient_SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated(); [Theory, MemberData(nameof(UseSsl_MemberData))] public Task ZeroByteReceive_CompletesWhenDataAvailable(Uri server) From 131ad9aae5fb56fe703b092fb93444445443c9f5 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Sat, 28 Jun 2025 01:21:25 +0100 Subject: [PATCH 10/20] Refactoring --- .../tests/AbortTest.Loopback.cs | 97 +++---- .../tests/AbortTest.cs | 36 +-- .../tests/CancelTest.Loopback.cs | 61 +--- .../tests/CancelTest.cs | 34 +-- .../tests/ClientWebSocketOptionsTests.cs | 4 +- .../tests/ClientWebSocketTestBase.cs | 81 ++---- .../tests/CloseTest.Loopback.cs | 146 ++++------ .../tests/CloseTest.cs | 93 ++----- .../tests/ConnectTest.Http2.cs | 168 ++++++----- .../tests/ConnectTest.Loopback.cs | 261 +++++------------- .../tests/ConnectTest.SharedHandler.cs | 177 ++++++++++++ .../tests/ConnectTest.cs | 189 ++++--------- .../tests/DeflateTests.cs | 14 +- .../tests/KeepAliveTest.Loopback.cs | 30 +- .../tests/KeepAliveTest.cs | 4 +- .../LoopbackWebSocketServer.Echo.cs | 12 +- .../LoopbackWebSocketServer.Http.cs | 2 +- .../LoopbackServer/LoopbackWebSocketServer.cs | 16 +- .../tests/SendReceiveTest.Http2.cs | 12 +- .../tests/SendReceiveTest.Loopback.cs | 16 +- .../tests/SendReceiveTest.cs | 31 +-- .../System.Net.WebSockets.Client.Tests.csproj | 3 + .../tests/wasm/BrowserTimerThrottlingTest.cs | 4 +- 23 files changed, 594 insertions(+), 897 deletions(-) create mode 100644 src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.SharedHandler.cs 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 530df78923f9e8..95d534f2be7f5f 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs @@ -10,9 +10,31 @@ 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_LoopbackBase : AbortTestBase + public abstract class AbortTest_Loopback(ITestOutputHelper output) : AbortTestBase(output) { - public AbortTest_LoopbackBase(ITestOutputHelper output) : base(output) { } + // --- Loopback Echo Server "overrides" --- + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage(bool useSsl) => RunEchoAsync( + RunClient_Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task Abort_SendAndAbort_Success(bool useSsl) => RunEchoAsync( + RunClient_Abort_SendAndAbort_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task Abort_ReceiveAndAbort_Success(bool useSsl) => RunEchoAsync( + RunClient_Abort_ReceiveAndAbort_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task Abort_CloseAndAbort_Success(bool useSsl) => RunEchoAsync( + RunClient_Abort_CloseAndAbort_Success, useSsl); + + [Theory, MemberData(nameof(UseSsl_MemberData))] + public Task ClientWebSocket_Abort_CloseOutputAsync(bool useSsl) => RunEchoAsync( + RunClient_ClientWebSocket_Abort_CloseOutputAsync, useSsl); + + // --- public static object[][] AbortClient_MemberData = ToMemberData(Enum.GetValues(), UseSsl_Values, /* verifySendReceive */ Bool_Values); @@ -188,92 +210,37 @@ protected static async Task VerifySendReceiveAsync(WebSocket ws, byte[] localMsg } } - // --- Loopback Echo Server "overrides" --- - - public abstract class AbortTest_Loopback : AbortTest_LoopbackBase - { - public AbortTest_Loopback(ITestOutputHelper output) : base(output) { } - - [Theory, MemberData(nameof(UseSsl_MemberData))] - public Task Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage(bool useSsl) => RunEchoAsync( - RunClient_Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage, useSsl); - - [Theory, MemberData(nameof(UseSsl_MemberData))] - public Task Abort_SendAndAbort_Success(bool useSsl) => RunEchoAsync( - RunClient_Abort_SendAndAbort_Success, useSsl); - - [Theory, MemberData(nameof(UseSsl_MemberData))] - public Task Abort_ReceiveAndAbort_Success(bool useSsl) => RunEchoAsync( - RunClient_Abort_ReceiveAndAbort_Success, useSsl); - - [Theory, MemberData(nameof(UseSsl_MemberData))] - public Task Abort_CloseAndAbort_Success(bool useSsl) => RunEchoAsync( - RunClient_Abort_CloseAndAbort_Success, useSsl); - - [Theory, MemberData(nameof(UseSsl_MemberData))] - public Task ClientWebSocket_Abort_CloseOutputAsync(bool useSsl) => RunEchoAsync( - RunClient_ClientWebSocket_Abort_CloseOutputAsync, useSsl); - } - // --- HTTP/1.1 WebSocket loopback tests --- - public sealed class AbortTest_Invoker_Loopback : AbortTest_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_Invoker_Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseCustomInvoker => true; } - public sealed class AbortTest_HttpClient_Loopback : AbortTest_Loopback + public sealed class AbortTest_HttpClient_Loopback(ITestOutputHelper output) : AbortTest_Loopback(output) { - public AbortTest_HttpClient_Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseHttpClient => true; } - // TODO - public sealed class AbortTest_SharedHandler_Loopback : AbortTest_LoopbackBase //! - { - public AbortTest_SharedHandler_Loopback(ITestOutputHelper output) : base(output) { } - - [Fact] - public Task Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage() => RunEchoAsync( - RunClient_Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage, useSsl: false); - - [Fact] - public Task Abort_SendAndAbort_Success() => RunEchoAsync( - RunClient_Abort_SendAndAbort_Success, useSsl: false); - - [Fact] - public Task Abort_ReceiveAndAbort_Success() => RunEchoAsync( - RunClient_Abort_ReceiveAndAbort_Success, useSsl: false); - - [Fact] - public Task Abort_CloseAndAbort_Success() => RunEchoAsync( - RunClient_Abort_CloseAndAbort_Success, useSsl: false); - - [Fact] - public Task ClientWebSocket_Abort_CloseOutputAsync() => RunEchoAsync( - RunClient_ClientWebSocket_Abort_CloseOutputAsync, useSsl: false); - } - // --- HTTP/2 WebSocket loopback tests --- - public abstract class AbortTest_Http2Loopback : AbortTest_Loopback + public abstract class AbortTest_Http2Loopback(ITestOutputHelper output) : AbortTest_Loopback(output) { - public AbortTest_Http2Loopback(ITestOutputHelper output) : base(output) { } internal 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); } - public sealed class AbortTest_Invoker_Http2Loopback : AbortTest_Http2Loopback + public sealed class AbortTest_Invoker_Http2Loopback(ITestOutputHelper output) : AbortTest_Http2Loopback(output) { - public AbortTest_Invoker_Http2Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseCustomInvoker => true; } - public sealed class AbortTest_HttpClient_Http2Loopback : AbortTest_Http2Loopback + public sealed class AbortTest_HttpClient_Http2Loopback(ITestOutputHelper output) : AbortTest_Http2Loopback(output) { - public AbortTest_HttpClient_Http2Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseHttpClient => true; } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs index c07faf2188d73e..4ca39ab946f0d8 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs @@ -9,24 +9,8 @@ namespace System.Net.WebSockets.Client.Tests { - public sealed class InvokerAbortTest : AbortTest + public abstract class AbortTestBase(ITestOutputHelper output) : ClientWebSocketTestBase(output) { - 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 abstract class AbortTestBase : ClientWebSocketTestBase - { - public AbortTestBase(ITestOutputHelper output) : base(output) { } - protected async Task RunClient_Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage(Uri server) { using (var cws = new ClientWebSocket()) @@ -134,14 +118,10 @@ await cws.SendAsync( } } - // --- External Echo Server "overrides" --- - [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] - public class AbortTest : AbortTestBase + public abstract class AbortTest_External(ITestOutputHelper output) : AbortTestBase(output) { - public AbortTest(ITestOutputHelper output) : base(output) { } - [Theory, MemberData(nameof(EchoServers))] public Task Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage(Uri server) => RunClient_Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage(server); @@ -162,4 +142,16 @@ public Task Abort_CloseAndAbort_Success(Uri server) public Task ClientWebSocket_Abort_CloseOutputAsync(Uri server) => RunClient_ClientWebSocket_Abort_CloseOutputAsync(server); } + + 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; + } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.Loopback.cs index 6b052a3356ff76..5cd8bb063849b7 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.Loopback.cs @@ -12,10 +12,8 @@ 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 : CancelTestBase + public abstract class CancelTest_Loopback(ITestOutputHelper output) : CancelTestBase(output) { - public CancelTest_Loopback(ITestOutputHelper output) : base(output) { } - [Theory, MemberData(nameof(UseSsl_MemberData))] public Task ConnectAsync_Cancel_ThrowsCancellationException(bool useSsl) => RunEchoAsync( RunClient_ConnectAsync_Cancel_ThrowsCancellationException, useSsl); @@ -51,69 +49,32 @@ public Task ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketExceptio // --- HTTP/1.1 WebSocket loopback tests --- - public sealed class CancelTest_Invoker_Loopback : CancelTest_Loopback + public sealed class CancelTest_SharedHandler_Loopback(ITestOutputHelper output) : CancelTest_Loopback(output) { } + + public sealed class CancelTest_Invoker_Loopback(ITestOutputHelper output) : CancelTest_Loopback(output) { - public CancelTest_Invoker_Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseCustomInvoker => true; } - public sealed class CancelTest_HttpClient_Loopback : CancelTest_Loopback + public sealed class CancelTest_HttpClient_Loopback(ITestOutputHelper output) : CancelTest_Loopback(output) { - public CancelTest_HttpClient_Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseHttpClient => true; } - // TODO - public sealed class CancelTest_SharedHandler_Loopback : CancelTestBase //! - { - public CancelTest_SharedHandler_Loopback(ITestOutputHelper output) : base(output) { } - - [Fact] - public Task ConnectAsync_Cancel_ThrowsCancellationException() => RunEchoAsync( - RunClient_ConnectAsync_Cancel_ThrowsCancellationException, useSsl: false); - - [Fact] - public Task SendAsync_Cancel_Success() => RunEchoAsync( - RunClient_SendAsync_Cancel_Success, useSsl: false); - - [Fact] - public Task ReceiveAsync_Cancel_Success() => RunEchoAsync( - RunClient_ReceiveAsync_Cancel_Success, useSsl: false); - - [Fact] - public Task CloseAsync_Cancel_Success() => RunEchoAsync( - RunClient_CloseAsync_Cancel_Success, useSsl: false); - - [Fact] - public Task CloseOutputAsync_Cancel_Success() => RunEchoAsync( - RunClient_CloseOutputAsync_Cancel_Success, useSsl: false); - - [Fact] - public Task ReceiveAsync_CancelThenReceive_ThrowsOperationCanceledException() => RunEchoAsync( - RunClient_ReceiveAsync_CancelThenReceive_ThrowsOperationCanceledException, useSsl: false); - - [Fact] - public Task ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException() => RunEchoAsync( - RunClient_ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException, useSsl: false); + // --- HTTP/2 WebSocket loopback tests --- - [Fact] - public Task ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException() => RunEchoAsync( - RunClient_ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException, useSsl: false); + public abstract class CancelTest_Http2Loopback(ITestOutputHelper output) : CancelTest_Loopback(output) + { + internal override Version HttpVersion => Net.HttpVersion.Version20; } - // --- HTTP/2 WebSocket loopback tests --- - - public sealed class CancelTest_Invoker_Http2Loopback : CancelTest_Loopback + public sealed class CancelTest_Invoker_Http2Loopback(ITestOutputHelper output) : CancelTest_Http2Loopback(output) { - public CancelTest_Invoker_Http2Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseCustomInvoker => true; - internal override Version HttpVersion => Net.HttpVersion.Version20; } - public sealed class CancelTest_HttpClient_Http2Loopback : CancelTest_Loopback + public sealed class CancelTest_HttpClient_Http2Loopback(ITestOutputHelper output) : CancelTest_Http2Loopback(output) { - public CancelTest_HttpClient_Http2Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseHttpClient => true; - internal override Version HttpVersion => Net.HttpVersion.Version20; } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs index 844aa5d62705a9..30027eabf409cf 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs @@ -9,24 +9,8 @@ namespace System.Net.WebSockets.Client.Tests { - public sealed class InvokerCancelTest : CancelTest + public abstract class CancelTestBase(ITestOutputHelper output) : ClientWebSocketTestBase(output) { - 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 abstract class CancelTestBase : ClientWebSocketTestBase - { - public CancelTestBase(ITestOutputHelper output) : base(output) { } - protected async Task RunClient_ConnectAsync_Cancel_ThrowsCancellationException(Uri server) { using (var cws = new ClientWebSocket()) @@ -166,10 +150,8 @@ protected async Task RunClient_ReceiveAsync_AfterCancellationDoReceiveAsync_Thro [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] - public class CancelTest : CancelTestBase + public class CancelTest_External(ITestOutputHelper output) : CancelTestBase(output) { - public CancelTest(ITestOutputHelper output) : base(output) { } - [ActiveIssue("https://github.com/dotnet/runtime/issues/83579", typeof(PlatformDetection), nameof(PlatformDetection.IsNodeJS))] [Theory, MemberData(nameof(EchoServers))] public Task ConnectAsync_Cancel_ThrowsCancellationException(Uri server) @@ -203,4 +185,16 @@ public Task ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException(Uri public Task ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException(Uri server) => RunClient_ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException(server); } + + 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; + } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs index 7a39f2423cad86..84cb90d9b36908 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs @@ -13,10 +13,8 @@ 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() diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs index 157619cd7d8662..0f7314cd6228cb 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs @@ -27,8 +27,8 @@ public class ClientWebSocketTestBase new object[] { o[0], true } }).ToArray(); - public static readonly bool[] Bool_Values = new[] { false, true }; - public static readonly bool[] UseSsl_Values = PlatformDetection.SupportsAlpn ? Bool_Values : new[] { false }; + public static readonly bool[] Bool_Values = [ false, true ]; + public static readonly bool[] UseSsl_Values = PlatformDetection.SupportsAlpn ? Bool_Values : [ false ]; public static readonly object[][] UseSsl_MemberData = ToMemberData(UseSsl_Values); public static readonly object[][] UseSslAndBoolean = ToMemberData(UseSsl_Values, Bool_Values); @@ -121,23 +121,6 @@ protected static async Task ReceiveEntireMessageAsync(We } } - /*private TestConfig? _config = null!; - protected TestConfig Config => _config ??= new TestConfig - { - InvokerType = InvokerType, - ConfigureHttpHandler = ConfigureCustomHandler, - HttpVersion = HttpVersion, - };*/ - - /*protected bool? UseSsl { get; set; } - protected Uri? ServerUri { get; set; } - - protected HttpInvokerType InvokerType => UseCustomInvoker - ? HttpInvokerType.HttpMessageInvoker - : UseHttpClient - ? HttpInvokerType.HttpClient - : HttpInvokerType.Shared;*/ - protected virtual bool UseCustomInvoker => false; protected virtual bool UseHttpClient => false; @@ -187,7 +170,7 @@ protected Task ConnectAsync(ClientWebSocket cws, Uri uri, CancellationToken canc } protected Task TestEcho(Uri uri, WebSocketMessageType type) - => WebSocketHelper.TestEcho(uri, type, TimeOutMilliseconds, _output,o => ConfigureHttpVersion(o, uri), GetInvoker()); + => WebSocketHelper.TestEcho(uri, type, TimeOutMilliseconds, _output, o => ConfigureHttpVersion(o, uri), GetInvoker()); protected void ConfigureHttpVersion(ClientWebSocketOptions options, Uri uri) { @@ -222,58 +205,26 @@ protected void ConfigureHttpVersion(ClientWebSocketOptions options, Uri uri) public static bool WebSocketsSupported { get { return WebSocketHelper.WebSocketsSupported; } } - /*public record class TestConfig - { - public HttpInvokerType InvokerType { get; set; } - public Action? ConfigureHttpHandler { get; set; } - - private HttpMessageInvoker? _invoker = null; - public HttpMessageInvoker? Invoker => _invoker ??= CreateInvoker(); - - public Version HttpVersion { get; set; } - public bool? UseSsl { get; set; } - public Uri? Uri { get; set; } +#if !TARGET_BROWSER + // Loopback server related functions + protected virtual bool SkipIfUseSsl => false; - private HttpMessageInvoker? CreateInvoker() + protected Task RunEchoAsync(Func clientFunc, bool useSsl) + { + if (SkipIfUseSsl && useSsl) { - if (InvokerType == HttpInvokerType.Shared) - { - return null; - } - - var handler = new HttpClientHandler(); - - if (PlatformDetection.IsNotBrowser) - { - handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator; - } - - ConfigureHttpHandler?.Invoke(handler); - - return InvokerType switch - { - HttpInvokerType.HttpClient => new HttpClient(handler), - HttpInvokerType.HttpMessageInvoker => new HttpMessageInvoker(handler), - _ => throw new NotImplementedException() - }; + throw new SkipTestException("SSL is not supported in this test."); } - + return LoopbackWebSocketServer.RunEchoAsync(clientFunc, HttpVersion, useSsl, TimeOutMilliseconds, _output); } - public enum HttpInvokerType - { - Shared = 0, - HttpClient, - HttpMessageInvoker - }*/ - -#if !TARGET_BROWSER - // Loopback server related functions - protected Task RunEchoAsync(Func clientFunc, bool useSsl) - => LoopbackWebSocketServer.RunEchoAsync(clientFunc, HttpVersion, useSsl, TimeOutMilliseconds); - protected Task RunEchoHeadersAsync(Func clientFunc, bool useSsl) { + if (SkipIfUseSsl && useSsl) + { + throw new SkipTestException("SSL is not supported in this test."); + } + var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); var options = new LoopbackWebSocketServer.Options { diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs index 02e8fcc1ea5e47..f7f74d2b5a0a1a 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs @@ -5,6 +5,7 @@ using System.Net.Test.Common; using System.Threading; using System.Threading.Tasks; +using Microsoft.DotNet.RemoteExecutor; using Xunit; using Xunit.Abstractions; @@ -14,11 +15,9 @@ 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_Loopback : CloseTestBase + public abstract class CloseTest_Loopback(ITestOutputHelper output) : CloseTestBase(output) { - public CloseTest_Loopback(ITestOutputHelper output) : base(output) { } - - [Theory, MemberData(nameof(UseSslAndBoolean))] + [Theory, MemberData(nameof(UseSslAndBoolean))] // to move to loopback public Task CloseAsync_ServerInitiatedClose_Success(bool useSsl, bool useCloseOutputAsync) => RunEchoAsync( server => RunClient_CloseAsync_ServerInitiatedClose_Success(server, useCloseOutputAsync), useSsl); @@ -54,15 +53,15 @@ public Task CloseAsync_CloseOutputAsync_Throws(bool useSsl) => RunEchoAsync( public Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(bool useSsl) => RunEchoAsync( RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanClose, useSsl); - [Theory, MemberData(nameof(UseSslAndBoolean))] + [Theory, MemberData(nameof(UseSslAndBoolean))]// to move to loopback public Task CloseOutputAsync_ServerInitiated_CanReceive(bool useSsl, bool delayReceiving) => RunEchoAsync( server => RunClient_CloseOutputAsync_ServerInitiated_CanReceive(server, delayReceiving), useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl_MemberData))]// to move to loopback public Task CloseOutputAsync_ServerInitiated_CanSend(bool useSsl) => RunEchoAsync( RunClient_CloseOutputAsync_ServerInitiated_CanSend, useSsl); - [Theory, MemberData(nameof(UseSslAndBoolean))] + [Theory, MemberData(nameof(UseSslAndBoolean))]// to move to loopback public Task CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(bool useSsl, bool syncState) => RunEchoAsync( server => RunClient_CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(server, syncState), useSsl); @@ -70,11 +69,10 @@ public Task CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(bool useSsl, b public Task CloseOutputAsync_CloseDescriptionIsNull_Success(bool useSsl) => RunEchoAsync( RunClient_CloseOutputAsync_CloseDescriptionIsNull_Success, useSsl); - // TODO - /*[ActiveIssue("https://github.com/dotnet/runtime/issues/22000", TargetFrameworkMonikers.Netcoreapp)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/22000", TargetFrameworkMonikers.Netcoreapp)] [Theory, MemberData(nameof(UseSsl_MemberData))] public Task CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates(bool useSsl) => RunEchoAsync( - RunClient_CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates, useSsl);*/ + RunClient_CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates, useSsl); [Theory, MemberData(nameof(UseSsl_MemberData))] public Task CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates(bool useSsl) => RunEchoAsync( @@ -83,24 +81,8 @@ public Task CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates(bool useSsl) // --- HTTP/1.1 WebSocket loopback tests --- - public sealed class CloseTest_Invoker_Loopback : CloseTest_Loopback + public sealed class CloseTest_SharedHandler_Loopback(ITestOutputHelper output) : CloseTest_Loopback(output) { - public CloseTest_Invoker_Loopback(ITestOutputHelper output) : base(output) { } - protected override bool UseCustomInvoker => true; - } - - public sealed class CloseTest_HttpClient_Loopback : CloseTest_Loopback - { - public CloseTest_HttpClient_Loopback(ITestOutputHelper output) : base(output) { } - protected override bool UseHttpClient => true; - } - - // TODO - public sealed class CloseTest_SharedHandler_Loopback : CloseTestBase //! - { - public CloseTest_SharedHandler_Loopback(ITestOutputHelper output) : base(output) { } - - // TODO [Fact] public async Task CloseAsync_CancelableEvenWhenPendingReceive_Throws() { @@ -147,82 +129,68 @@ await Assert.ThrowsAnyAsync(async () => }), new LoopbackServer.Options { WebSocketEndpoint = true }); } - [Theory, InlineData(false), InlineData(true)] - public Task CloseAsync_ServerInitiatedClose_Success(bool useCloseOutputAsync) => RunEchoAsync( - server => RunClient_CloseAsync_ServerInitiatedClose_Success(server, useCloseOutputAsync), useSsl: false); - - [Fact] - public Task CloseAsync_ClientInitiatedClose_Success() => RunEchoAsync( - RunClient_CloseAsync_ClientInitiatedClose_Success, useSsl: false); - - [Fact] - public Task CloseAsync_CloseDescriptionIsMaxLength_Success() => RunEchoAsync( - RunClient_CloseAsync_CloseDescriptionIsMaxLength_Success, useSsl: false); - - [Fact] - public Task CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException() => RunEchoAsync( - RunClient_CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException, useSsl: false); - - [Fact] - public Task CloseAsync_CloseDescriptionHasUnicode_Success() => RunEchoAsync( - RunClient_CloseAsync_CloseDescriptionHasUnicode_Success, useSsl: false); - - [Fact] - public Task CloseAsync_CloseDescriptionIsNull_Success() => RunEchoAsync( - RunClient_CloseAsync_CloseDescriptionIsNull_Success, useSsl: false); - - [Fact] - public Task CloseOutputAsync_ExpectedStates() => RunEchoAsync( - RunClient_CloseOutputAsync_ExpectedStates, useSsl: false); - - [Fact] - public Task CloseAsync_CloseOutputAsync_Throws() => RunEchoAsync( - RunClient_CloseAsync_CloseOutputAsync_Throws, useSsl: false); - - [Fact] - public Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose() => RunEchoAsync( - RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanClose, useSsl: false); - - [Theory, InlineData(false), InlineData(true)] - public Task CloseOutputAsync_ServerInitiated_CanReceive(bool delayReceiving) => RunEchoAsync( - server => RunClient_CloseOutputAsync_ServerInitiated_CanReceive(server, delayReceiving), useSsl: false); + // 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_External test = (CloseTest_External)Activator.CreateInstance(typeof(CloseTest_External).Assembly.GetType(typeName), new object[] { null }); + using CancellationTokenSource timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); - [Fact] - public Task CloseOutputAsync_ServerInitiated_CanSend() => RunEchoAsync( - RunClient_CloseOutputAsync_ServerInitiated_CanSend, useSsl: false); + Exception unobserved = null; + TaskScheduler.UnobservedTaskException += (obj, args) => + { + unobserved = args.Exception; + }; - [Theory, InlineData(false), InlineData(true)] - public Task CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(bool syncState) => RunEchoAsync( - server => RunClient_CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(server, syncState), useSsl: false); + TaskCompletionSource clientCompleted = new TaskCompletionSource(); - [Fact] - public Task CloseOutputAsync_CloseDescriptionIsNull_Success() => RunEchoAsync( - RunClient_CloseOutputAsync_CloseDescriptionIsNull_Success, useSsl: false); + 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 = Net.HttpVersion.Version11, UseSsl = true, HttpInvoker = test.GetInvoker() }, timeoutCts.Token); + }, GetType().FullName).DisposeAsync(); + } + } - // TODO - /*[ActiveIssue("https://github.com/dotnet/runtime/issues/22000", TargetFrameworkMonikers.Netcoreapp)] - [Fact] - public Task CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates() => RunEchoAsync( - RunClient_CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates, useSsl: false);*/ + public sealed class CloseTest_Invoker_Loopback(ITestOutputHelper output) : CloseTest_Loopback(output) + { + protected override bool UseCustomInvoker => true; + } - [Fact] - public Task CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates() => RunEchoAsync( - RunClient_CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates, useSsl: false); + public sealed class CloseTest_HttpClient_Loopback(ITestOutputHelper output) : CloseTest_Loopback(output) + { + protected override bool UseHttpClient => true; } // --- HTTP/2 WebSocket loopback tests --- - public sealed class CloseTest_Invoker_Http2Loopback : CloseTest_Loopback + public abstract class CloseTest_Http2Loopback(ITestOutputHelper output) : CloseTest_Loopback(output) { - public CloseTest_Invoker_Http2Loopback(ITestOutputHelper output) : base(output) { } - protected override bool UseCustomInvoker => true; internal override Version HttpVersion => Net.HttpVersion.Version20; } - public sealed class CloseTest_HttpClient_Http2Loopback : CloseTest_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) { - public CloseTest_HttpClient_Http2Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseHttpClient => true; - internal override Version HttpVersion => Net.HttpVersion.Version20; } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs index 556501eaf7d821..db584151a857d3 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs @@ -9,29 +9,13 @@ using Xunit; using Xunit.Abstractions; -using Microsoft.DotNet.RemoteExecutor; namespace System.Net.WebSockets.Client.Tests { - public sealed class InvokerCloseTest : CloseTest + public abstract class CloseTestBase(ITestOutputHelper output) : ClientWebSocketTestBase(output) { - public InvokerCloseTest(ITestOutputHelper output) : base(output) { } - - protected override bool UseCustomInvoker => true; - } - - public sealed class HttpClientCloseTest : CloseTest - { - public HttpClientCloseTest(ITestOutputHelper output) : base(output) { } - - protected override bool UseHttpClient => true; - } - - public abstract class CloseTestBase : ClientWebSocketTestBase - { - public CloseTestBase(ITestOutputHelper output) : base(output) { } - - protected async Task RunClient_CloseAsync_ServerInitiatedClose_Success(Uri server, bool useCloseOutputAsync) + // [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] + protected async Task RunClient_CloseAsync_ServerInitiatedClose_Success(Uri server, bool useCloseOutputAsync) // -- TO MOVE to loopback and close issue { const string shutdownWebSocketMetaCommand = ".shutdown"; @@ -240,6 +224,7 @@ protected async Task RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanCl } } + // [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] -- TO MOVE to loopback and close issue protected async Task RunClient_CloseOutputAsync_ServerInitiated_CanReceive(Uri server, bool delayReceiving) { var expectedCloseStatus = WebSocketCloseStatus.NormalClosure; @@ -294,6 +279,7 @@ await cws.SendAsync( } } + // [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] -- TO MOVE to loopback and close issue protected async Task RunClient_CloseOutputAsync_ServerInitiated_CanSend(Uri server) { string message = "Hello WebSockets!"; @@ -339,6 +325,7 @@ await cws.SendAsync( } } + // [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] -- TO MOVE to loopback and close issue protected async Task RunClient_CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(Uri server, bool syncState) { using (ClientWebSocket cws = await GetConnectedWebSocket(server)) @@ -380,6 +367,7 @@ protected async Task RunClient_CloseOutputAsync_CloseDescriptionIsNull_Success(U } } + // [ActiveIssue("https://github.com/dotnet/runtime/issues/22000", TargetFrameworkMonikers.Netcoreapp)] protected async Task RunClient_CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates(Uri server) { var receiveBuffer = new byte[1024]; @@ -443,15 +431,8 @@ protected async Task RunClient_CloseAsync_DuringConcurrentReceiveAsync_ExpectedS [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] - public class CloseTest : CloseTestBase + public abstract class CloseTest_External(ITestOutputHelper output) : CloseTestBase(output) { - public CloseTest(ITestOutputHelper output) : base(output) { } - - [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] - [Theory, MemberData(nameof(EchoServersAndBoolean))] - public Task CloseAsync_ServerInitiatedClose_Success(Uri server, bool useCloseOutputAsync) - => RunClient_CloseAsync_ServerInitiatedClose_Success(server, useCloseOutputAsync); - [Theory, MemberData(nameof(EchoServers))] public Task CloseAsync_ClientInitiatedClose_Success(Uri server) => RunClient_CloseAsync_ClientInitiatedClose_Success(server); @@ -484,21 +465,6 @@ public Task CloseAsync_CloseOutputAsync_Throws(Uri server) public Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(Uri server) => RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanClose(server); - [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] - [Theory, MemberData(nameof(EchoServersAndBoolean))] - public Task CloseOutputAsync_ServerInitiated_CanReceive(Uri server, bool delayReceiving) - => RunClient_CloseOutputAsync_ServerInitiated_CanReceive(server, delayReceiving); - - [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] - [Theory, MemberData(nameof(EchoServers))] - public Task CloseOutputAsync_ServerInitiated_CanSend(Uri server) - => RunClient_CloseOutputAsync_ServerInitiated_CanSend(server); - - [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] - [Theory, 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); @@ -511,42 +477,17 @@ public Task CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates(Uri ser [Theory, MemberData(nameof(EchoServers))] public Task CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates(Uri server) => RunClient_CloseAsync_DuringConcurrentReceiveAsync_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); - - Exception unobserved = null; - TaskScheduler.UnobservedTaskException += (obj, args) => - { - unobserved = args.Exception; - }; + public sealed class CloseTest_SharedHandler_External(ITestOutputHelper output) : CloseTest_External(output) { } - TaskCompletionSource clientCompleted = new TaskCompletionSource(); + public sealed class CloseTest_Invoker_External(ITestOutputHelper output) : CloseTest_External(output) + { + protected override bool UseCustomInvoker => true; + } - 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 = Net.HttpVersion.Version11, UseSsl = true, HttpInvoker = test.GetInvoker() }, timeoutCts.Token); - }, GetType().FullName).DisposeAsync(); - } + public sealed class CloseTest_HttpClient_External(ITestOutputHelper output) : CloseTest_External(output) + { + protected override bool UseHttpClient => true; } } 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 3b033b9cbb2324..ec857ea54fa91e 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Http2.cs @@ -12,53 +12,11 @@ namespace System.Net.WebSockets.Client.Tests { - public sealed class InvokerConnectTest_Http2 : ConnectTest_Http2 - { - 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 = 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 }; - } + // --- HTTP/2 WebSocket loopback tests --- - [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 abstract class ConnectTest_Http2Loopback(ITestOutputHelper output) : ConnectTest_Loopback(output) { - public ConnectTest_Http2(ITestOutputHelper output) : base(output) { } + internal override Version HttpVersion => Net.HttpVersion.Version20; [Fact] [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets is not supported on this platform")] @@ -112,46 +70,6 @@ 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 = 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); - } - } - - [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 = Net.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() @@ -228,6 +146,46 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => }, new Http2Options() { WebSocketEndpoint = true, UseSsl = false }); } + [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 = 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); + } + } + + [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 = Net.HttpVersion.Version20; + cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; + await cws.ConnectAsync(server, GetInvoker(), cts.Token); + Assert.Equal(WebSocketState.Open, cws.State); + } + } + [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [Theory] [MemberData(nameof(EchoServers))] @@ -246,7 +204,7 @@ public async Task ConnectAsync_Http11WithRequestVersionOrHigher_DowngradeSuccess [Fact] [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets is not supported on this platform")] - public async Task ConnectAsync_Http11WithRequestVersionOrHigher_Loopback_Success() + public async Task ConnectAsync_Http11WithRequestVersionOrHigher_Loopback_DowngradeSuccess() { await LoopbackServer.CreateServerAsync(async (server, url) => { @@ -269,4 +227,42 @@ await server.AcceptConnectionAsync(async connection => }, new LoopbackServer.Options { UseSsl = true, WebSocketEndpoint = true }); } } + + 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; + } + + public sealed class ConnectTest_SharedHandler_Http2(ITestOutputHelper output) : ClientWebSocketTestBase(output) + { + 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); + } + } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs index e0cddb6fc05698..1ed9053bd894cf 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.IO; +using System.Net.Http; using System.Net.Test.Common; using System.Runtime.InteropServices; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Microsoft.DotNet.XUnitExtensions; @@ -15,17 +17,10 @@ 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_Loopback : ConnectTestBase + public abstract class ConnectTest_Loopback(ITestOutputHelper output) : ConnectTestBase(output) { - public ConnectTest_Loopback(ITestOutputHelper output) : base(output) { } - // --- Loopback Echo Server "overrides" --- - //[ActiveIssue("https://github.com/dotnet/runtime/issues/1895")] - //[Theory, MemberData(nameof(UnavailableWebSocketServers))] - //public Task ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(Uri server, string exceptionMessage, WebSocketError errorCode) - // => RunClient_ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(server, exceptionMessage, errorCode); - [Theory, MemberData(nameof(UseSsl_MemberData))] public Task EchoBinaryMessage_Success(bool useSsl) => RunEchoAsync( RunClient_EchoBinaryMessage_Success, useSsl); @@ -49,224 +44,94 @@ public Task ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketExcepti [Theory, MemberData(nameof(UseSsl_MemberData))] public Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol(bool useSsl) => RunEchoAsync( RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol, useSsl); - - // TODO: this test is HTTP/1.1 only - //[Theory, MemberData(nameof(UseSsl_MemberData))] - //public Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(bool useSsl) => RunEchoAsync( - // RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState, useSsl); } // --- HTTP/1.1 WebSocket loopback tests --- - // TODO - public abstract class ConnectTest_Loopback_Http11Only : ConnectTest_Loopback + public abstract class ConnectTest_Loopback_Http11(ITestOutputHelper output) : ConnectTest_Loopback(output) { - public ConnectTest_Loopback_Http11Only(ITestOutputHelper output) : base(output) { } - - // TODO: this test is HTTP/1.1 only [Theory, MemberData(nameof(UseSsl_MemberData))] public Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(bool useSsl) => RunEchoAsync( RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState, useSsl); } - public sealed class ConnectTest_Invoker_Loopback : ConnectTest_Loopback_Http11Only //! + public sealed class ConnectTest_Invoker_Loopback(ITestOutputHelper output) : ConnectTest_Loopback_Http11(output) { - public ConnectTest_Invoker_Loopback(ITestOutputHelper output) : base(output) { } protected override bool UseCustomInvoker => true; - } - - public sealed class ConnectTest_HttpClient_Loopback : ConnectTest_Loopback_Http11Only //! - { - public ConnectTest_HttpClient_Loopback(ITestOutputHelper output) : base(output) { } - protected override bool UseHttpClient => true; - } - - // TODO - public sealed class ConnectTest_SharedHandler_Loopback : ConnectTestBase //! - { - public ConnectTest_SharedHandler_Loopback(ITestOutputHelper output) : base(output) { } - - // TODO - [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() + public static IEnumerable ConnectAsync_CustomInvokerWithIncompatibleWebSocketOptions_ThrowsArgumentException_MemberData() { - await LoopbackServer.CreateClientAndServerAsync(async uri => + 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) { - 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 }); + 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 }; } - [Fact] - public async Task ConnectAsync_HttpResponseDetailsCollectedOnFailure_CustomHeader() + [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) { - await LoopbackServer.CreateClientAndServerAsync(async uri => + using var invoker = new HttpMessageInvoker(new SocketsHttpHandler { - 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); + ConnectCallback = (_, _) => ValueTask.FromException(new Exception("ConnectCallback")) + }); - 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 }); - } + using var ws = new ClientWebSocket(); + configureOptions(ws.Options); - [Fact] - public async Task ConnectAsync_HttpResponseDetailsCollectedOnSuccess_Extensions() - { - await LoopbackServer.CreateClientAndServerAsync(async uri => + Task connectTask = ws.ConnectAsync(new Uri("wss://dummy"), invoker, CancellationToken.None); + if (shouldThrow) { - 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 => + Assert.Equal(TaskStatus.Faulted, connectTask.Status); + await Assert.ThrowsAsync("options", () => connectTask); + } + else { - Dictionary headers = await LoopbackHelper.WebSocketHandshakeAsync(connection, "X-CustomHeader1"); - }), new LoopbackServer.Options { WebSocketEndpoint = true }); - } + WebSocketException ex = await Assert.ThrowsAsync(() => connectTask); + Assert.NotNull(ex.InnerException); + Assert.Contains("ConnectCallback", ex.InnerException.Message); + } - [Fact] - public async Task ConnectAsync_AddHostHeader_Success() - { - string expectedHost = null; - await LoopbackServer.CreateClientAndServerAsync(async uri => + foreach (X509Certificate cert in ws.Options.ClientCertificates) { - 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 }); + cert.Dispose(); + } } - - //[ActiveIssue("https://github.com/dotnet/runtime/issues/1895")] - //[Theory, MemberData(nameof(UnavailableWebSocketServers))] - //public Task ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(Uri server, string exceptionMessage, WebSocketError errorCode) - // => RunClient_ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(server, exceptionMessage, errorCode); - - [Fact] - public Task EchoBinaryMessage_Success() => RunEchoAsync( - RunClient_EchoBinaryMessage_Success, useSsl: false); - - [Fact] - public Task EchoTextMessage_Success() => RunEchoAsync( - RunClient_EchoTextMessage_Success, useSsl: false); - - [Fact] - public Task ConnectAsync_AddCustomHeaders_Success() => RunEchoHeadersAsync( - RunClient_ConnectAsync_AddCustomHeaders_Success, useSsl: false); - - [Fact] - public Task ConnectAsync_CookieHeaders_Success() => RunEchoHeadersAsync( - RunClient_ConnectAsync_CookieHeaders_Success, useSsl: false); - - /*[Fact] - public Task ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException() => RunEchoAsync( - RunClient_ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException, useSsl: false);*/ - - [Fact] - public Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol() => RunEchoAsync( - RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol, useSsl: false); - - [Fact] - public Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState() => RunEchoAsync( - RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState, useSsl: false); - } - - // --- HTTP/2 WebSocket loopback tests --- - - public sealed class ConnectTest_Invoker_Http2 : ConnectTest_Loopback - { - public ConnectTest_Invoker_Http2(ITestOutputHelper output) : base(output) { } - protected override bool UseCustomInvoker => true; - internal override Version HttpVersion => Net.HttpVersion.Version20; } - /*public sealed class ConnectTest_HttpClient_Http2 : ConnectTest_Loopback + public sealed class ConnectTest_HttpClient_Loopback(ITestOutputHelper output) : ConnectTest_Loopback_Http11(output) { - public ConnectTest_HttpClient_Http2(ITestOutputHelper output) : base(output) { } protected override bool UseHttpClient => true; - internal override Version HttpVersion => Net.HttpVersion.Version20; - }*/ + } } 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..256f1f2f7c3d2a --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.SharedHandler.cs @@ -0,0 +1,177 @@ +// 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.Runtime.InteropServices; +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 sealed class ConnectTest_SharedHandler_Loopback(ITestOutputHelper output) : ConnectTest_Loopback_Http11(output) + { + [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 }); + } + } +} diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs index 5d50cb2979d54e..94f3af1a1b8778 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs @@ -8,122 +8,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 sealed class InvokerConnectTest : ConnectTest + 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); - - // 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(); - } - } - } - - public sealed class HttpClientConnectTest : ConnectTest - { - public HttpClientConnectTest(ITestOutputHelper output) : base(output) { } - - protected override bool UseHttpClient => true; - } - - public abstract class ConnectTestBase : ClientWebSocketTestBase - { - public ConnectTestBase(ITestOutputHelper output) : base(output) { } - - protected async Task RunClient_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)); - } - } - protected async Task RunClient_EchoBinaryMessage_Success(Uri server) { await TestEcho(server, WebSocketMessageType.Binary); @@ -270,6 +162,11 @@ protected async Task RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequi protected async Task RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(Uri server) { + if (HttpVersion != Net.HttpVersion.Version11) + { + throw new SkipTestException("LoopbackProxyServer is HTTP/1.1 only"); + } + using (var cws = new ClientWebSocket()) using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) using (LoopbackProxyServer proxyServer = LoopbackProxyServer.Create()) @@ -292,43 +189,12 @@ protected async Task RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClose Assert.Equal(1, proxyServer.Connections); } } - - [ConditionalFact(nameof(WebSocketsSupported))] - 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); - } - } - - [ConditionalFact(nameof(WebSocketsSupported))] - 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); - } - } } [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] - public class ConnectTest : ConnectTestBase + public abstract class ConnectTest_External(ITestOutputHelper output) : ConnectTestBase(output) { - public ConnectTest(ITestOutputHelper output) : base(output) { } - - [ActiveIssue("https://github.com/dotnet/runtime/issues/1895")] - [Theory, MemberData(nameof(UnavailableWebSocketServers))] - public Task ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(Uri server, string exceptionMessage, WebSocketError errorCode) - => RunClient_ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(server, exceptionMessage, errorCode); - [Theory, MemberData(nameof(EchoServers))] public Task EchoBinaryMessage_Success(Uri server) => RunClient_EchoBinaryMessage_Success(server); @@ -360,5 +226,44 @@ public Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesA [Theory, MemberData(nameof(EchoServers))] public Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(Uri server) => RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(server); + + [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 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)); + } + } + } + + 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; } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/DeflateTests.cs b/src/libraries/System.Net.WebSockets.Client/tests/DeflateTests.cs index 7c8f68ed1f5d99..7811463565a5f6 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/DeflateTests.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/DeflateTests.cs @@ -14,27 +14,19 @@ namespace System.Net.WebSockets.Client.Tests { - public sealed class InvokerDeflateTests : DeflateTests + public sealed class InvokerDeflateTests(ITestOutputHelper output) : DeflateTests(output) { - public InvokerDeflateTests(ITestOutputHelper output) : base(output) { } - protected override bool UseCustomInvoker => true; } - public sealed class HttpClientDeflateTests : DeflateTests + public sealed class HttpClientDeflateTests(ITestOutputHelper output) : DeflateTests(output) { - public HttpClientDeflateTests(ITestOutputHelper output) : base(output) { } - protected override bool UseHttpClient => true; } [PlatformSpecific(~TestPlatforms.Browser)] - public class DeflateTests : ClientWebSocketTestBase + public 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")] 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 7239e8188c62f3..b2c9e7a82f1622 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs @@ -10,10 +10,8 @@ namespace System.Net.WebSockets.Client.Tests { [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) { } - [OuterLoop("Uses Task.Delay")] [Theory] [MemberData(nameof(UseSsl_MemberData))] @@ -109,34 +107,32 @@ private static async Task VerifySendReceiveAsync(WebSocket ws, byte[] localMsg, // --- HTTP/1.1 WebSocket loopback tests --- - public class KeepAliveTest_Invoker_Loopback : KeepAliveTest_Loopback + public class KeepAliveTest_SharedHandler_Loopback(ITestOutputHelper output) : KeepAliveTest_Loopback(output) { } + + public 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 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) { } - } - // --- HTTP/2 WebSocket loopback tests --- - public class KeepAliveTest_Invoker_Http2 : KeepAliveTest_Invoker_Loopback + public class KeepAliveTest_Http2Loopback(ITestOutputHelper output) : KeepAliveTest_Loopback(output) { - public KeepAliveTest_Invoker_Http2(ITestOutputHelper output) : base(output) { } internal override Version HttpVersion => Net.HttpVersion.Version20; } - public class KeepAliveTest_HttpClient_Http2 : KeepAliveTest_HttpClient_Loopback + public class KeepAliveTest_Invoker_Http2Loopback(ITestOutputHelper output) : KeepAliveTest_Http2Loopback(output) { - public KeepAliveTest_HttpClient_Http2(ITestOutputHelper output) : base(output) { } - internal override Version HttpVersion => Net.HttpVersion.Version20; + protected override bool UseCustomInvoker => true; + } + + public class KeepAliveTest_HttpClient_Http2Loopback(ITestOutputHelper output) : KeepAliveTest_Http2Loopback(output) + { + protected override bool UseHttpClient => true; } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs index 5ff9c51e56a6af..da4e2438a0b3ab 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs @@ -14,10 +14,8 @@ 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() 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 index d119c5d766ec7c..ac2269327ceb52 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Echo.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Echo.cs @@ -5,12 +5,13 @@ using System.Threading; using System.Threading.Tasks; using Xunit; +using Xunit.Abstractions; namespace System.Net.WebSockets.Client.Tests { public static partial class LoopbackWebSocketServer { - public static Task RunEchoAsync(Func loopbackClientFunc, Version httpVersion, bool useSsl, int timeOutMilliseconds) + public static Task RunEchoAsync(Func loopbackClientFunc, Version httpVersion, bool useSsl, int timeOutMilliseconds, ITestOutputHelper output) { var timeoutCts = new CancellationTokenSource(timeOutMilliseconds); var options = new Options @@ -20,9 +21,12 @@ public static Task RunEchoAsync(Func loopbackClientFunc, Version http SkipServerHandshakeResponse = true, // to negotiate subprotocols and extensions IgnoreServerErrors = true, AbortServerOnClientExit = true, - ParseEchoOptions = true + ParseEchoOptions = true, + Output = output }; + output.WriteLine("RunEchoAsync called with options: " + options); + return RunAsyncPrivate( loopbackClientFunc, (data, token) => RunEchoServerWebSocketAsync(data, options, token), @@ -43,6 +47,8 @@ private static async Task RunEchoServerWebSocketAsync(WebSocketRequestData data, await SendNegotiatedServerResponseAsync(data, options, cancellationToken).ConfigureAwait(false); + options.Output?.WriteLine("EchoServer: connection established"); + await RunServerWebSocketAsync( data, (serverWebSocket, token) => WebSocketEchoHelper.RunEchoAll( @@ -52,6 +58,8 @@ await RunServerWebSocketAsync( token), options, cancellationToken).ConfigureAwait(false); + + options.Output?.WriteLine("EchoServer: server function completed"); } } } 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 index 0fd2c87cda241f..91281acb99bb62 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs @@ -30,7 +30,7 @@ private static Task RunClientAndServerAsync( if (options.HttpVersion == HttpVersion.Version20) { - var http2Options = new Http2Options { WebSocketEndpoint = true, UseSsl = options.UseSsl }; + var http2Options = new Http2Options { WebSocketEndpoint = true, UseSsl = options.UseSsl, EnsureThreadSafeIO = true }; options.ConfigureHttp2Options?.Invoke(http2Options); return Http2LoopbackServer.CreateClientAndServerAsync( 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 e066133296e58f..7412f027e485a8 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Xunit; +using Xunit.Abstractions; namespace System.Net.WebSockets.Client.Tests { @@ -59,10 +60,13 @@ private static Task RunAsyncPrivate( { try { + options.Output?.WriteLine("RunAsyncPrivate: starting client function"); await loopbackClientFunc(uri); + options.Output?.WriteLine("RunAsyncPrivate: client function completed"); } finally { + options.Output?.WriteLine("RunAsyncPrivate: cancelling client exit token"); clientExitCts.Cancel(); } }, @@ -81,12 +85,20 @@ private static async Task RunServerWebSocketAsync( var wsOptions = new WebSocketCreationOptions { IsServer = true, SubProtocol = options.ServerSubProtocol }; var serverWebSocket = WebSocket.CreateFromStream(requestData.TransportStream, wsOptions); - using var registration = cancellationToken.Register(serverWebSocket.Abort); + using var registration = cancellationToken.Register(() => { + options.Output?.WriteLine("RunServerWebSocketAsync: aborting server on cancellation"); + serverWebSocket.Abort(); + }); + + options.Output?.WriteLine("RunServerWebSocketAsync: starting server function"); await serverWebSocketFunc(serverWebSocket, cancellationToken).ConfigureAwait(false); + options.Output?.WriteLine("RunServerWebSocketAsync: server function completed"); + if (options.DisposeServerWebSocket) { + options.Output?.WriteLine("RunServerWebSocketAsync: disposing server WebSocket"); serverWebSocket.Dispose(); } } @@ -148,6 +160,8 @@ public record class Options() public bool DisposeClientWebSocket { get; set; } public bool DisposeHttpInvoker { get; set; } public Action? ConfigureClientOptions { get; set; } + + public ITestOutputHelper? Output { get; set; } } } } 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 39cd184afebc3e..44abfe13ba927a 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Http2.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Http2.cs @@ -12,24 +12,18 @@ namespace System.Net.WebSockets.Client.Tests { - public sealed class HttpClientSendReceiveTest_Http2 : SendReceiveTest_Http2 + public sealed class HttpClientSendReceiveTest_Http2(ITestOutputHelper output) : SendReceiveTest_Http2(output) { - public HttpClientSendReceiveTest_Http2(ITestOutputHelper output) : base(output) { } - protected override bool UseHttpClient => true; } - public sealed class InvokerSendReceiveTest_Http2 : SendReceiveTest_Http2 + public sealed class InvokerSendReceiveTest_Http2(ITestOutputHelper output) : SendReceiveTest_Http2(output) { - public InvokerSendReceiveTest_Http2(ITestOutputHelper output) : base(output) { } - protected override bool UseCustomInvoker => true; } - public abstract class SendReceiveTest_Http2 : ClientWebSocketTestBase + public abstract class SendReceiveTest_Http2(ITestOutputHelper output) : ClientWebSocketTestBase(output) { - public SendReceiveTest_Http2(ITestOutputHelper output) : base(output) { } - [Fact] [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets is not supported on this platform")] public async Task ReceiveNoThrowAfterSend_NoSsl() diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs index d81975be8e80eb..258daed0b87f64 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs @@ -13,10 +13,8 @@ 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_Loopback : SendReceiveTestBase + public abstract class SendReceiveTest_Loopback(ITestOutputHelper output) : SendReceiveTestBase(output) { - public SendReceiveTest(ITestOutputHelper output) : base(output) { } - [Theory, MemberData(nameof(UseSsl_MemberData))] public Task SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(Uri server) => RunClient_SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(server); @@ -58,11 +56,9 @@ public Task ZeroByteReceive_CompletesWhenDataAvailable(Uri server) => RunClient_ZeroByteReceive_CompletesWhenDataAvailable(server); } - public class MemorySendReceiveTest : SendReceiveTest - { - public MemorySendReceiveTest(ITestOutputHelper output) : base(output) { } - - protected override async Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) + public class MemorySendReceiveTest(ITestOutputHelper output) : SendReceiveTest(output) + { + protected override async Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) { ValueWebSocketReceiveResult r = await ws.ReceiveAsync( (Memory)arraySegment, @@ -78,10 +74,8 @@ protected override Task SendAsync(WebSocket ws, ArraySegment arraySegment, cancellationToken).AsTask(); } - public class ArraySegmentSendReceiveTest : SendReceiveTest + public class ArraySegmentSendReceiveTest(ITestOutputHelper output) : SendReceiveTest(output) { - public ArraySegmentSendReceiveTest(ITestOutputHelper output) : base(output) { } - protected override Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) => ws.ReceiveAsync(arraySegment, cancellationToken); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs index 7f2404eb932d8b..c5ae5180dc2a1f 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs @@ -12,37 +12,28 @@ namespace System.Net.WebSockets.Client.Tests { - public sealed class InvokerMemorySendReceiveTest : MemorySendReceiveTest + public sealed class InvokerMemorySendReceiveTest(ITestOutputHelper output) : MemorySendReceiveTest(output) { - public InvokerMemorySendReceiveTest(ITestOutputHelper output) : base(output) { } - protected override bool UseCustomInvoker => true; } - public sealed class HttpClientMemorySendReceiveTest : MemorySendReceiveTest + public sealed class HttpClientMemorySendReceiveTest(ITestOutputHelper output) : MemorySendReceiveTest(output) { - public HttpClientMemorySendReceiveTest(ITestOutputHelper output) : base(output) { } - protected override bool UseHttpClient => true; } - public sealed class InvokerArraySegmentSendReceiveTest : ArraySegmentSendReceiveTest + public sealed class InvokerArraySegmentSendReceiveTest(ITestOutputHelper output) : ArraySegmentSendReceiveTest(output) { - public InvokerArraySegmentSendReceiveTest(ITestOutputHelper output) : base(output) { } - protected override bool UseCustomInvoker => true; } - public sealed class HttpClientArraySegmentSendReceiveTest : ArraySegmentSendReceiveTest + public sealed class HttpClientArraySegmentSendReceiveTest(ITestOutputHelper output) : ArraySegmentSendReceiveTest(output) { - public HttpClientArraySegmentSendReceiveTest(ITestOutputHelper output) : base(output) { } - protected override bool UseHttpClient => true; } - public class MemorySendReceiveTest : SendReceiveTest + public class MemorySendReceiveTest(ITestOutputHelper output) : SendReceiveTest(output) { - public MemorySendReceiveTest(ITestOutputHelper output) : base(output) { } protected override async Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) { @@ -60,10 +51,8 @@ protected override Task SendAsync(WebSocket ws, ArraySegment arraySegment, cancellationToken).AsTask(); } - public class ArraySegmentSendReceiveTest : SendReceiveTest + public class ArraySegmentSendReceiveTest(ITestOutputHelper output) : SendReceiveTest(output) { - public ArraySegmentSendReceiveTest(ITestOutputHelper output) : base(output) { } - protected override Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) => ws.ReceiveAsync(arraySegment, cancellationToken); @@ -71,13 +60,11 @@ protected override Task SendAsync(WebSocket ws, ArraySegment arraySegment, ws.SendAsync(arraySegment, messageType, endOfMessage, cancellationToken); } - public abstract class SendReceiveTestBase : ClientWebSocketTestBase + public abstract class SendReceiveTestBase(ITestOutputHelper output) : ClientWebSocketTestBase(output) { 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 SendReceiveTestBase(ITestOutputHelper output) : base(output) { } - protected async Task RunClient_SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(Uri server) { const int SendBufferSize = 10; @@ -508,10 +495,8 @@ protected async Task RunClient_ZeroByteReceive_CompletesWhenDataAvailable(Uri se [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] - public abstract class SendReceiveTest : SendReceiveTestBase + public abstract class SendReceiveTest(ITestOutputHelper output) : SendReceiveTestBase(output) { - public SendReceiveTest(ITestOutputHelper output) : base(output) { } - [Theory, MemberData(nameof(EchoServers))] public Task SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(Uri server) => RunClient_SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(server); 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 b83338f7056377..c09f9bbae9683f 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 @@ -66,6 +68,7 @@ + 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..79fa2b4765cddc 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 From ce4a8be9263ec98d1a50e1c10a27e96b5f4fcadb Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Tue, 1 Jul 2025 11:44:23 +0100 Subject: [PATCH 11/20] Fix H2 loopback --- .../System/Net/Http/GenericLoopbackServer.cs | 1 + .../Net/Http/Http2LoopbackConnection.cs | 23 +++-- .../Helpers/WebSocketEchoHelper.cs | 73 +++++++++------- .../tests/AbortTest.Loopback.cs | 6 +- .../tests/ClientWebSocketTestBase.cs | 84 ++++++++++++++++--- .../tests/CloseTest.cs | 12 +-- .../tests/ConnectTest.cs | 2 + .../tests/KeepAliveTest.Loopback.cs | 3 +- .../LoopbackServer/Http2LoopbackStream.cs | 20 ++++- .../LoopbackWebSocketServer.Echo.cs | 13 +-- .../LoopbackWebSocketServer.Http.cs | 44 ++++++++-- .../LoopbackServer/LoopbackWebSocketServer.cs | 71 ++++++++++++---- .../WebSocketHandshakeHelper.cs | 11 ++- .../tests/WebSocketHelper.cs | 16 ++-- 14 files changed, 281 insertions(+), 98 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs index 93a9c6318b601e..114bfa21332a57 100644 --- a/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs @@ -188,6 +188,7 @@ public class GenericLoopbackOptions #if !NETSTANDARD2_0 && !NETFRAMEWORK public SslStreamCertificateContext? CertificateContext { get; set; } #endif + public TextWriter? Logger { get; set; } } public struct HttpHeaderData diff --git a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs index 8a3a52fee9c5be..8ab96ae0ea7310 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs @@ -30,6 +30,7 @@ public class Http2LoopbackConnection : GenericLoopbackConnection private bool _expectClientDisconnect; private readonly SemaphoreSlim? _readLock; private readonly SemaphoreSlim? _writeLock; + private readonly TextWriter? _logger; private readonly byte[] _prefix = new byte[24]; public string PrefixString => Encoding.UTF8.GetString(_prefix, 0, _prefix.Length); @@ -43,6 +44,7 @@ private Http2LoopbackConnection(SocketWrapper socket, Stream stream, TimeSpan ti _connectionStream = stream; _timeout = timeout; _transparentPingResponse = httpOptions.EnableTransparentPingResponse; + _logger = httpOptions.Logger; if (httpOptions.EnsureThreadSafeIO) { @@ -66,29 +68,26 @@ private Http2LoopbackConnection(SocketWrapper socket, Stream stream, TimeSpan ti }, writeAsyncFunc: async (buffer, offset, count, cancellationToken) => { - await _writeLock.WaitAsync(cancellationToken); + await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { - await stream.WriteAsync(buffer, offset, count, cancellationToken); + await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + await stream.FlushAsync(cancellationToken).ConfigureAwait(false); } finally { _writeLock.Release(); } }, - flushAsyncFunc: async (cancellationToken) => + disposeFunc: (disposing) => { - await _writeLock.WaitAsync(cancellationToken); - try + if (disposing) { - await stream.FlushAsync(cancellationToken).ConfigureAwait(false); + // _logger?.WriteLine($"[Http2LoopbackConnection] Disposing HTTP/2 connection"); + stream.Dispose(); + // _logger?.WriteLine($"[Http2LoopbackConnection] Disposed"); } - finally - { - _writeLock.Release(); - } - }, - disposeFunc: (disposing) => stream.Dispose() + } ); } } diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs index 744996f0e20e5c..24c80b9b1c673e 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Net.WebSockets; using System.Text; +using System.IO; using System.Threading; using System.Threading.Tasks; @@ -12,21 +13,26 @@ namespace System.Net.Test.Common { public static class WebSocketEchoHelper { - public static async Task RunEchoAll(WebSocket socket, bool replyWithPartialMessages, bool replyWithEnhancedCloseMessage, CancellationToken cancellationToken = default) + public static async Task RunEchoAll(WebSocket socket, bool replyWithPartialMessages, bool replyWithEnhancedCloseMessage, TextWriter? logger = null, CancellationToken cancellationToken = default) { const int MaxBufferSize = 128 * 1024; var receiveBuffer = new byte[MaxBufferSize]; var throwAwayBuffer = new byte[MaxBufferSize]; + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] State={socket.State}, starting receive loop..."); + // Stay in loop while websocket is open while (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseSent) { - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Waiting for messages..."); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] State={socket.State}, waiting for messages..."); var receiveResult = await socket.ReceiveAsync(new ArraySegment(receiveBuffer), cancellationToken); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Received {receiveResult.MessageType} frame, Count={receiveResult.Count}, EOM={receiveResult.EndOfMessage}"); + if (receiveResult.MessageType == WebSocketMessageType.Close) { + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] CloseStatus={receiveResult.CloseStatus}, CloseStatusDescription={receiveResult.CloseStatusDescription}"); if (receiveResult.CloseStatus == WebSocketCloseStatus.Empty) { await socket.CloseAsync(WebSocketCloseStatus.Empty, null, cancellationToken); @@ -41,6 +47,7 @@ await socket.CloseAsync( receiveResult.CloseStatusDescription, cancellationToken); } + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Closed"); continue; } @@ -49,7 +56,7 @@ await socket.CloseAsync( int offset = receiveResult.Count; while (receiveResult.EndOfMessage == false) { - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Incomplete message, waiting for continuations..."); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] EOM=false, waiting for continuation (offset={offset})..."); if (offset < MaxBufferSize) { receiveResult = await socket.ReceiveAsync( @@ -69,12 +76,12 @@ await socket.CloseAsync( // Close socket if the message was too big. if (offset > MaxBufferSize) { - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Full close (MessageTooBig)"); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Bad message, closing (MessageTooBig)"); await socket.CloseAsync( WebSocketCloseStatus.MessageTooBig, string.Format("{0}: {1} > {2}", WebSocketCloseStatus.MessageTooBig.ToString(), offset, MaxBufferSize), cancellationToken); - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Closed"); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Closed"); continue; } @@ -84,82 +91,84 @@ await socket.CloseAsync( if (receiveResult.MessageType == WebSocketMessageType.Text) { receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, offset); - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Received message: '{receivedMessage}'"); if (receivedMessage == ".close") { - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Full close..."); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Requested: Close..."); await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Closed"); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Closed"); } else if (receivedMessage == ".shutdown") { - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Shutdown writes only..."); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Requested: Shutdown writes..."); await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Writes closed"); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Writes closed"); } else if (receivedMessage == ".abort") { + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Requested: Abort..."); socket.Abort(); - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Aborted"); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Aborted"); } else if (receivedMessage == ".delay5sec") { - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Waiting for 5 seconds without sending any messages."); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Requested: Delay 5s..."); await Task.Delay(5000); - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Wait completed."); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Wait completed."); } else if (receivedMessage == ".receiveMessageAfterClose") { + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Requested: Echo timestamp and close..."); string message = $"{receivedMessage} {DateTime.Now:HH:mm:ss}"; - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Sending message: '{message}'"); byte[] buffer = Encoding.UTF8.GetBytes(message); await socket.SendAsync( new ArraySegment(buffer, 0, message.Length), WebSocketMessageType.Text, true, cancellationToken); - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Sent"); - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Full close..."); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Sent: '{message}', closing..."); await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Closed"); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Closed"); } else if (socket.State == WebSocketState.Open) { + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Text message: '{receivedMessage}'"); sendMessage = true; } } else { - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Received message type {receiveResult.MessageType}"); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Binary message (size='{offset}')"); sendMessage = true; } if (sendMessage) { - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Echo message"); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Echo..."); await socket.SendAsync( new ArraySegment(receiveBuffer, 0, offset), receiveResult.MessageType, !replyWithPartialMessages, cancellationToken); - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Sent"); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Sent"); } if (receivedMessage == ".closeafter") { - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Full close..."); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Requested: Close after echo..."); await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Closed"); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Closed"); } else if (receivedMessage == ".shutdownafter") { - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Shutdown writes only..."); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Requested: Shutdown writes after echo..."); await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); - //Console.WriteLine($"[Server - {nameof(RunEchoAll)} Writes closed"); + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Writes closed"); } } + + //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] State={socket.State}, Receive loop completed."); } - public static async Task RunEchoHeaders(WebSocket socket, IEnumerable> headers, CancellationToken cancellationToken = default) + public static async Task RunEchoHeaders(WebSocket socket, IEnumerable> headers, TextWriter? logger = null, CancellationToken cancellationToken = default) { const int MaxBufferSize = 1024; var receiveBuffer = new byte[MaxBufferSize]; @@ -175,15 +184,21 @@ public static async Task RunEchoHeaders(WebSocket socket, IEnumerable(sendBuffer), WebSocketMessageType.Text, true, cancellationToken); + //logger?.WriteLine($"[Server - {nameof(RunEchoHeaders)}] State={socket.State}, starting receive loop..."); + // Stay in loop while websocket is open while (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseSent) { + logger?.WriteLine($"[Server - {nameof(RunEchoHeaders)}] State={socket.State}, waiting for messages..."); var receiveResult = await socket.ReceiveAsync(new ArraySegment(receiveBuffer), cancellationToken); if (receiveResult.MessageType == WebSocketMessageType.Close) { + logger?.WriteLine($"[Server - {nameof(RunEchoHeaders)}] Received close frame, CloseStatus={receiveResult.CloseStatus}, CloseStatusDescription={receiveResult.CloseStatusDescription}"); if (receiveResult.CloseStatus == WebSocketCloseStatus.Empty) { await socket.CloseAsync(WebSocketCloseStatus.Empty, null, cancellationToken); @@ -199,15 +214,17 @@ await socket.CloseAsync( continue; } } + + logger?.WriteLine($"[Server - {nameof(RunEchoHeaders)}] State={socket.State}, Receive loop completed."); } - public static async ValueTask ProcessOptions(string queryString, CancellationToken cancellationToken = default) + public static async ValueTask ProcessOptions(string queryString, TextWriter? logger = null, CancellationToken cancellationToken = default) { - //Console.WriteLine($"Server: Processing echo oprions from query string = '{queryString}'"); + //logger?.WriteLine($"[Server - {nameof(ProcessOptions)}] Processing echo options from query string = '{queryString}'"); WebSocketEchoOptions options = WebSocketEchoOptions.Parse(queryString); if (options.Delay is TimeSpan d) { - //Console.WriteLine($"Server: delay = {d}"); + //logger?.WriteLine($"[Server - {nameof(ProcessOptions)}] delay = {d}"); await Task.Delay(d, cancellationToken).ConfigureAwait(false); } return options; 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 95d534f2be7f5f..85a459a8437801 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs @@ -86,7 +86,8 @@ public Task AbortClient_ServerGetsCorrectException(AbortType abortType, bool use HttpVersion = HttpVersion, UseSsl = useSsl, HttpInvoker = GetInvoker(), - DisposeServerWebSocket = true + DisposeServerWebSocket = true, + TestOutputHelper = _output }, timeoutCts.Token); } @@ -109,7 +110,8 @@ public Task ServerPrematureEos_ClientGetsCorrectException(ServerEosType serverEo HttpVersion = HttpVersion, UseSsl = useSsl, HttpInvoker = null, - SkipServerHandshakeResponse = true + SkipServerHandshakeResponse = true, + TestOutputHelper = _output }; var serverReceivedEosTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs index 0f7314cd6228cb..58b67c03bb1665 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Net.Http; +using System.Net.Sockets; using System.Net.Test.Common; using System.Reflection; using System.Threading; @@ -128,6 +130,7 @@ protected static async Task ReceiveEntireMessageAsync(We protected bool UseSharedHandler => !UseCustomInvoker && !UseHttpClient; protected Action? ConfigureCustomHandler; + protected bool UseSocketsHttpHandler = true;//false; internal virtual Version HttpVersion => Net.HttpVersion.Version11; @@ -138,14 +141,67 @@ protected static async Task ReceiveEntireMessageAsync(We return null; } - var handler = new HttpClientHandler(); + HttpMessageHandler handler; - if (PlatformDetection.IsNotBrowser) + if (UseSocketsHttpHandler && SocketsHttpHandler.IsSupported) { - handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; + if (ConfigureCustomHandler is not null) + { + throw new InvalidOperationException("ConfigureCustomHandler is not supported when UseSocketsHttpHandler is true."); + } + + var shh = new SocketsHttpHandler(); + shh.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true; + shh.ConnectCallback = async (context, ct) => + { + // Create and connect a socket using default settings. + Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; + try + { + await socket.ConnectAsync(context.DnsEndPoint, ct).ConfigureAwait(false); + Stream stream = new NetworkStream(socket, ownsSocket: true); +/*#if !TARGET_BROWSER + var networkStream = stream; + var delegatingStream = new DelegateDelegatingStream(networkStream); + delegatingStream.DisposeFunc = disposing => + { + //_output.WriteLine($"[Client] NetworkStream.Dispose({disposing})"); + if (disposing) + { + networkStream.Dispose(); + } + }; + delegatingStream.DisposeAsyncFunc = () => + { + //_output.WriteLine($"[Client] NetworkStream.DisposeAsync()"); + return networkStream.DisposeAsync(); + }; + stream = delegatingStream; +#endif*/ + return stream; + } + catch + { + socket.Dispose(); + throw; + } + }; + handler = shh; + } + else if (PlatformDetection.IsNotBrowser) + { + var httpClientHandler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (_, _, _, _) => true + }; + ConfigureCustomHandler?.Invoke(httpClientHandler); + handler = httpClientHandler; + } + else + { + handler = new HttpClientHandler(); } - ConfigureCustomHandler?.Invoke(handler); if (UseCustomInvoker) { @@ -231,22 +287,30 @@ protected Task RunEchoHeadersAsync(Func clientFunc, bool useSsl) HttpVersion = HttpVersion, UseSsl = useSsl, IgnoreServerErrors = true, - AbortServerOnClientExit = true + AbortServerOnClientExit = true, + TestOutputHelper = _output }; return LoopbackWebSocketServer.RunAsync( clientFunc, async (requestData, token) => { - // Console.WriteLine($"[Server - {nameof(RunEchoHeadersAsync)}] WebSocket.CreateFromStream"); + // _output?.WriteLine($"[Server - {nameof(RunEchoHeadersAsync)}] WebSocket.CreateFromStream"); var serverWebSocket = WebSocket.CreateFromStream( requestData.TransportStream, new WebSocketCreationOptions { IsServer = true }); - using var registration = token.Register(serverWebSocket.Abort); - // Console.WriteLine($"[Server - {nameof(RunEchoHeadersAsync)}] RunEchoHeaders"); - await WebSocketEchoHelper.RunEchoHeaders(serverWebSocket, requestData.Headers, token); - // Console.WriteLine($"[Server - {nameof(RunEchoHeadersAsync)}] RunEchoHeaders completed"); + using (var registration = token.Register(() => { + // _output?.WriteLine($"[Server - {nameof(RunEchoHeadersAsync)}] Aborting server WebSocket on cancellation"); + serverWebSocket.Abort(); + })) + { + // _output?.WriteLine($"[Server - {nameof(RunEchoHeadersAsync)}] RunEchoHeaders"); + await WebSocketEchoHelper.RunEchoHeaders(serverWebSocket, requestData.Headers, options.Logger, token); + // _output?.WriteLine($"[Server - {nameof(RunEchoHeadersAsync)}] RunEchoHeaders completed"); + } + + // _output?.WriteLine($"[Server - Run Echo Headers WS] Completed; Server WebSocket state: {serverWebSocket.State}"); }, options, timeoutCts.Token); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs index db584151a857d3..e6d19cc28e6782 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs @@ -23,18 +23,18 @@ protected async Task RunClient_CloseAsync_ServerInitiatedClose_Success(Uri serve { var cts = new CancellationTokenSource(TimeOutMilliseconds); - _output.WriteLine("SendAsync starting."); + //_output.WriteLine("SendAsync starting."); await cws.SendAsync( WebSocketData.GetBufferFromText(shutdownWebSocketMetaCommand), WebSocketMessageType.Text, true, cts.Token); - _output.WriteLine("SendAsync done."); + //_output.WriteLine("SendAsync done."); var recvBuffer = new byte[256]; - _output.WriteLine("ReceiveAsync starting."); + //_output.WriteLine("ReceiveAsync starting."); WebSocketReceiveResult recvResult = await cws.ReceiveAsync(new ArraySegment(recvBuffer), cts.Token); - _output.WriteLine("ReceiveAsync done."); + //_output.WriteLine("ReceiveAsync done."); // Verify received server-initiated close message. Assert.Equal(WebSocketCloseStatus.NormalClosure, recvResult.CloseStatus); @@ -47,12 +47,12 @@ await cws.SendAsync( Assert.Equal(shutdownWebSocketMetaCommand, cws.CloseStatusDescription); // Send back close message to acknowledge server-initiated close. - _output.WriteLine("Close starting."); + //_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."); + //_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 diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs index 94f3af1a1b8778..e7a115d78df84c 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs @@ -84,6 +84,7 @@ protected async Task RunClient_ConnectAsync_CookieHeaders_Success(Uri server) else { ConfigureCustomHandler = handler => handler.CookieContainer = cookies; + UseSocketsHttpHandler = false; } using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) @@ -172,6 +173,7 @@ protected async Task RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClose using (LoopbackProxyServer proxyServer = LoopbackProxyServer.Create()) { ConfigureCustomHandler = handler => handler.Proxy = new WebProxy(proxyServer.Uri); + UseSocketsHttpHandler = false; if (UseSharedHandler) { 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 b2c9e7a82f1622..e67557b2225c52 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs @@ -37,7 +37,8 @@ public Task KeepAlive_LongDelayBetweenSendReceives_Succeeds(bool useSsl) clientOptions.KeepAliveInterval = TimeSpan.FromMilliseconds(100); clientOptions.KeepAliveTimeout = TimeSpan.FromSeconds(1); }, - ConfigureHttp2Options = http2Options => http2Options.EnsureThreadSafeIO = true + ConfigureHttp2Options = http2Options => http2Options.EnsureThreadSafeIO = true, + TestOutputHelper = _output }; return LoopbackWebSocketServer.RunAsync( diff --git a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/Http2LoopbackStream.cs b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/Http2LoopbackStream.cs index b841eead6ea248..5195cf2c901110 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/Http2LoopbackStream.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/Http2LoopbackStream.cs @@ -15,7 +15,9 @@ public class Http2LoopbackStream : Stream private readonly int _streamId; private bool _readEnded; private ReadOnlyMemory _leftoverReadData; + private bool _sendEosOnDispose; private bool _sendResetOnDispose; + private readonly TextWriter? _logger; public override bool CanRead => true; public override bool CanSeek => false; @@ -24,11 +26,13 @@ public class Http2LoopbackStream : Stream public Http2LoopbackConnection Connection => _connection; public int StreamId => _streamId; - public Http2LoopbackStream(Http2LoopbackConnection connection, int streamId, bool sendResetOnDispose = true) + public Http2LoopbackStream(Http2LoopbackConnection connection, int streamId, bool sendEosOnDispose = true, bool sendResetOnDispose = true, TextWriter? logger = null) { _connection = connection; _streamId = streamId; + _sendEosOnDispose = sendEosOnDispose; _sendResetOnDispose = sendResetOnDispose; + _logger = logger; } public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) @@ -43,6 +47,7 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation if (_readEnded) { + // _logger?.WriteLine($"[Http2LoopbackStream] Reached EOS"); return 0; } @@ -71,10 +76,19 @@ public override async ValueTask DisposeAsync() { try { - await _connection.SendResponseDataAsync(_streamId, Memory.Empty, endStream: true).ConfigureAwait(false); + //_logger?.WriteLine($"[Http2LoopbackStream] Disposing"); + + // _logger?.WriteLine($"[Http2LoopbackStream] Disposing, _readEnded: {_readEnded}, _sendResetOnDispose: {_sendResetOnDispose}"); + + if (_sendEosOnDispose) + { + // _logger?.WriteLine($"[Http2LoopbackStream] Sending EOS"); + await _connection.SendResponseDataAsync(_streamId, Memory.Empty, endStream: true).ConfigureAwait(false); + } if (!_readEnded && _sendResetOnDispose) { + // _logger?.WriteLine($"[Http2LoopbackStream] Sending RST_STREAM..."); var rstFrame = new RstStreamFrame(FrameFlags.None, (int)ProtocolErrors.NO_ERROR, _streamId); await _connection.WriteFrameAsync(rstFrame).ConfigureAwait(false); } @@ -87,6 +101,8 @@ public override async ValueTask DisposeAsync() { // Ignore connection errors } + // _logger?.WriteLine($"[Http2LoopbackStream] Disposed"); + //return default; } public override void Flush() { } 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 index ac2269327ceb52..022fbe76d41701 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Echo.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Echo.cs @@ -22,10 +22,10 @@ public static Task RunEchoAsync(Func loopbackClientFunc, Version http IgnoreServerErrors = true, AbortServerOnClientExit = true, ParseEchoOptions = true, - Output = output + TestOutputHelper = output }; - output.WriteLine("RunEchoAsync called with options: " + options); + //output.WriteLine("[Common - Run Echo WS] RunEchoAsync called with options: " + options); return RunAsyncPrivate( loopbackClientFunc, @@ -36,6 +36,8 @@ public static Task RunEchoAsync(Func loopbackClientFunc, Version http private static async Task RunEchoServerWebSocketAsync(WebSocketRequestData data, Options options, CancellationToken cancellationToken) { + //options.Logger?.WriteLine($"[Server - Run Echo WS] Starting..."); + Assert.NotNull(data.EchoOptions); WebSocketEchoOptions echoOptions = data.EchoOptions.Value; @@ -47,19 +49,20 @@ private static async Task RunEchoServerWebSocketAsync(WebSocketRequestData data, await SendNegotiatedServerResponseAsync(data, options, cancellationToken).ConfigureAwait(false); - options.Output?.WriteLine("EchoServer: connection established"); + //options.Logger?.WriteLine($"[Server - Run Echo WS] Connection established"); - await RunServerWebSocketAsync( + var webSocket = await RunServerWebSocketAsync( data, (serverWebSocket, token) => WebSocketEchoHelper.RunEchoAll( serverWebSocket, echoOptions.ReplyWithPartialMessages, echoOptions.ReplyWithEnhancedCloseMessage, + options.Logger, token), options, cancellationToken).ConfigureAwait(false); - options.Output?.WriteLine("EchoServer: server function completed"); + //options.Logger?.WriteLine($"[Server - Run Echo WS] Completed; Server WebSocket state: {webSocket.State}"); } } } 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 index 91281acb99bb62..080cb5e2e5d575 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs @@ -19,6 +19,8 @@ private static Task RunClientAndServerAsync( CancellationToken clientExitCt, CancellationToken globalCt) { + //options.Logger?.WriteLine($"[Common - {nameof(RunClientAndServerAsync)}] HTTP version: {options.HttpVersion}"); + if (options.HttpVersion == HttpVersion.Version11) { return LoopbackServer.CreateClientAndServerAsync( @@ -30,7 +32,7 @@ private static Task RunClientAndServerAsync( if (options.HttpVersion == HttpVersion.Version20) { - var http2Options = new Http2Options { WebSocketEndpoint = true, UseSsl = options.UseSsl, EnsureThreadSafeIO = true }; + var http2Options = new Http2Options { WebSocketEndpoint = true, UseSsl = options.UseSsl, EnsureThreadSafeIO = true, Logger = options.Logger }; options.ConfigureHttp2Options?.Invoke(http2Options); return Http2LoopbackServer.CreateClientAndServerAsync( @@ -51,14 +53,18 @@ private static Task ProcessHttp11WebSocketRequest( => http11server.AcceptConnectionAsync( async connection => { + //options.Logger?.WriteLine("[Server - Process HTTP/1.1] Processing handshake"); var requestData = await WebSocketHandshakeHelper.ProcessHttp11RequestAsync( connection, options.SkipServerHandshakeResponse, options.ParseEchoOptions, + options.Logger, cancellationToken).ConfigureAwait(false); - await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); - }); + //options.Logger?.WriteLine("[Server - Process HTTP/1.1] Running server callback"); + await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); + //options.Logger?.WriteLine("[Server - Process HTTP/1.1] Completed server callback"); + }); private static async Task ProcessHttp2WebSocketRequest( Http2LoopbackServer http2Server, @@ -66,26 +72,46 @@ private static async Task ProcessHttp2WebSocketRequest( Options options, CancellationToken cancellationToken) { + //options.Logger?.WriteLine("[Server - Process HTTP/2] Processing handshake"); var requestData = await WebSocketHandshakeHelper.ProcessHttp2RequestAsync( http2Server, options.SkipServerHandshakeResponse, options.ParseEchoOptions, + sendEosOnDispose: true,//!options.AbortServerOnClientExit, + options.Logger, cancellationToken).ConfigureAwait(false); + //options.Logger?.WriteLine("[Server - Process HTTP/2] Running server callback"); await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); - - DateTime now = DateTime.UtcNow; + //options.Logger?.WriteLine("[Server - Process HTTP/2] Completed server callback"); if (options.AbortServerOnClientExit) { + //options.Logger?.WriteLine("[Server - Process HTTP/2] Waiting for the client to exit..."); + var h2conn = requestData.Http2Connection!; + + + Frame frame = await h2conn.ReadFrameAsync(cancellationToken).ConfigureAwait(false); + if (frame is not null) + { + bool isRstStream = frame.Type == FrameType.RstStream; + bool isLastAck = frame.Type == FrameType.Data && ((DataFrame)frame).AckFlag; + if (!isRstStream && !isLastAck) + { + options.Logger?.WriteLine($"[Server - Process HTTP/2] Drained frame: {frame}"); + } + } + // Wait for the client to exit - await requestData.Http2Connection!.WaitForConnectionShutdownAsync(ignoreUnexpectedFrames: true).ConfigureAwait(false); + await h2conn.WaitForConnectionShutdownAsync(ignoreUnexpectedFrames: true).ConfigureAwait(false); } else { + //options.Logger?.WriteLine("[Server - Process HTTP/2] Send GOAWAY and shutdown..."); // This will send GOAWAY await requestData.Http2Connection!.ShutdownIgnoringErrorsAsync(requestData.Http2StreamId.Value).ConfigureAwait(false); } + //options.Logger?.WriteLine("[Server - Process HTTP/2] Shutdown completed"); } private static async Task RunHttpServer( @@ -99,14 +125,19 @@ private static async Task RunHttpServer( { try { + //options.Logger?.WriteLine("[Server - HTTP (generic)] Starting HTTP server..."); using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(globalCt, clientExitCt); await httpServerFunc(httpServer, wsServerFunc, options, linkedCts.Token) .WaitAsync(linkedCts.Token).ConfigureAwait(false); + + //options.Logger?.WriteLine("[Server - HTTP (generic)] Completed successfully."); } catch (Exception e) when (options.IgnoreServerErrors) { + //options.Logger?.WriteLine($"[Server - HTTP (generic)] Completed via IgnoreServerErrors ({e.GetType().Name})"); + if (e is OperationCanceledException && clientExitCt.IsCancellationRequested) { return; // expected for aborting on client exit @@ -130,6 +161,7 @@ await httpServerFunc(httpServer, wsServerFunc, options, linkedCts.Token) } Console.WriteLine($"[WARN] Server aborted on a WebSocketException ({we.WebSocketErrorCode}): {we.Message}"); + //options.Logger?.WriteLine($"[Server - HTTP (generic)][WARN] Server aborted on a WebSocketException ({we.WebSocketErrorCode}): {we.Message}"); return; // ignore } 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 7412f027e485a8..af741db07d7ab4 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs @@ -1,8 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; +using System.IO; using System.Net.Http; using System.Net.Test.Common; +using System.Text; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -49,24 +52,32 @@ private static Task RunAsyncPrivate( { if (!options.AbortServerOnClientExit) { + //options.Logger?.WriteLine("[Common - RunAsyncPrivate] Starting independent client and server"); return RunClientAndServerAsync( loopbackClientFunc, loopbackServerFunc, options, CancellationToken.None, globalCt); } CancellationTokenSource clientExitCts = new CancellationTokenSource(); + //options.Logger?.WriteLine("[Common - RunAsyncPrivate] Starting client and server"); + return RunClientAndServerAsync( async uri => { try { - options.Output?.WriteLine("RunAsyncPrivate: starting client function"); + //options.Logger?.WriteLine("[Client] starting client function"); await loopbackClientFunc(uri); - options.Output?.WriteLine("RunAsyncPrivate: client function completed"); + //options.Logger?.WriteLine("[Client] client function completed successfully"); } + //catch (Exception ex) + //{ + //options.Logger?.WriteLine($"[Client] client function failed with {ex}"); + // throw; + //} finally { - options.Output?.WriteLine("RunAsyncPrivate: cancelling client exit token"); + //options.Logger?.WriteLine("[Client] cancelling client exit token"); clientExitCts.Cancel(); } }, @@ -76,31 +87,40 @@ private static Task RunAsyncPrivate( globalCt); } - private static async Task RunServerWebSocketAsync( + private static async Task RunServerWebSocketAsync( WebSocketRequestData requestData, Func serverWebSocketFunc, Options options, CancellationToken cancellationToken) { + //options.Logger?.WriteLine("[Server - Run WS] Starting..."); + var wsOptions = new WebSocketCreationOptions { IsServer = true, SubProtocol = options.ServerSubProtocol }; var serverWebSocket = WebSocket.CreateFromStream(requestData.TransportStream, wsOptions); - using var registration = cancellationToken.Register(() => { - options.Output?.WriteLine("RunServerWebSocketAsync: aborting server on cancellation"); + using (var registration = cancellationToken.Register(() => + { + //options.Logger?.WriteLine("[Server - Run WS] Aborting server on cancellation"); serverWebSocket.Abort(); - }); - - options.Output?.WriteLine("RunServerWebSocketAsync: starting server function"); - - await serverWebSocketFunc(serverWebSocket, cancellationToken).ConfigureAwait(false); - - options.Output?.WriteLine("RunServerWebSocketAsync: server function completed"); + })) + { + //options.Logger?.WriteLine("[Server - Run WS] Starting server WS callback"); + await serverWebSocketFunc(serverWebSocket, cancellationToken).ConfigureAwait(false); + //options.Logger?.WriteLine("[Server - Run WS] server WS callback completed"); + } if (options.DisposeServerWebSocket) { - options.Output?.WriteLine("RunServerWebSocketAsync: disposing server WebSocket"); + //options.Logger?.WriteLine("[Server - Run WS] Disposing WebSocket"); serverWebSocket.Dispose(); } + else + { + //options.Logger?.WriteLine("[Server - Run WS] Not disposing WebSocket. State: " + serverWebSocket.State); + } + + //options.Logger?.WriteLine("[Server - Run WS] Completed"); + return serverWebSocket; } private static async Task RunClientWebSocketAsync( @@ -161,7 +181,28 @@ public record class Options() public bool DisposeHttpInvoker { get; set; } public Action? ConfigureClientOptions { get; set; } - public ITestOutputHelper? Output { get; set; } + private TestOutputWriter? _testOutputWriter; + public TextWriter? Logger => _testOutputWriter; + public ITestOutputHelper? TestOutputHelper { + get => _testOutputWriter; + set { _testOutputWriter = TestOutputWriter.Convert(value); } + } + + private class TestOutputWriter(ITestOutputHelper inner) : TextWriter, ITestOutputHelper + { + public override Encoding Encoding => Encoding.Unicode; + public override void Write(char value) => throw new NotSupportedException("Use WriteLine instead"); + public override void Write(char[] buffer, int index, int count) => throw new NotSupportedException("Use WriteLine instead"); + public override void WriteLine(string value) => inner.WriteLine(value); + public override void WriteLine(string format, params object[] args) => inner.WriteLine(format, args); + void ITestOutputHelper.WriteLine(string message) => inner.WriteLine(message); + void ITestOutputHelper.WriteLine(string format, params object[] args) => inner.WriteLine(format, args); + + public static TestOutputWriter? Convert(ITestOutputHelper? output) + => output is TestOutputWriter tow ? tow + : output is null ? null + : new TestOutputWriter(output); + } } } } 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 f869535e3f2b08..d3a9ae253a7684 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketHandshakeHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/WebSocketHandshakeHelper.cs @@ -7,6 +7,7 @@ using System.Net.Sockets; using System.Net.Test.Common; using System.Text; +using System.IO; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -19,6 +20,7 @@ public static async Task ProcessHttp11RequestAsync( LoopbackServer.Connection connection, bool skipServerHandshakeResponse = false, bool parseEchoOptions = false, + TextWriter? logger = null, CancellationToken cancellationToken = default) { List headers = await connection.ReadRequestHeaderAsync().WaitAsync(cancellationToken).ConfigureAwait(false); @@ -42,7 +44,7 @@ public static async Task ProcessHttp11RequestAsync( // 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).ConfigureAwait(false); + data.EchoOptions = await WebSocketEchoHelper.ProcessOptions(query, logger).ConfigureAwait(false); } else { @@ -96,6 +98,8 @@ public static async Task ProcessHttp2RequestAsync( Http2LoopbackServer server, bool skipServerHandshakeResponse = false, bool parseEchoOptions = false, + bool sendEosOnDispose = true, + TextWriter? logger = null, CancellationToken cancellationToken = default) { var connection = await server.EstablishConnectionAsync(new SettingsEntry { SettingId = SettingId.EnableConnect, Value = 1 }) @@ -133,7 +137,7 @@ public static async Task ProcessHttp2RequestAsync( { // 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).ConfigureAwait(false); + data.EchoOptions = await WebSocketEchoHelper.ProcessOptions(query, logger).ConfigureAwait(false); } else { @@ -154,7 +158,8 @@ public static async Task ProcessHttp2RequestAsync( await SendHttp2ServerResponseAsync(connection, streamId, cancellationToken: cancellationToken).ConfigureAwait(false); } - data.TransportStream = new Http2LoopbackStream(connection, streamId, sendResetOnDispose: false); + // logger?.WriteLine($"[Server - HTTP/2 WS Handshake] Created Http2LoopbackStream for streamId {streamId}, sendEosOnDispose: {sendEosOnDispose}"); + data.TransportStream = new Http2LoopbackStream(connection, streamId, sendEosOnDispose: sendEosOnDispose, sendResetOnDispose: false, logger); return data; } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs index a4805926ab9b27..c9181378298bf7 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs @@ -34,14 +34,14 @@ public static async Task TestEcho( using (ClientWebSocket cws = await GetConnectedWebSocket(server, timeOutMilliseconds, output, configureOptions, invoker)) { - output.WriteLine("TestEcho: SendAsync starting."); + //output.WriteLine("TestEcho: SendAsync starting."); await cws.SendAsync(WebSocketData.GetBufferFromText(message), type, true, cts.Token); - output.WriteLine("TestEcho: SendAsync done."); + //output.WriteLine("TestEcho: SendAsync done."); Assert.Equal(WebSocketState.Open, cws.State); - output.WriteLine("TestEcho: ReceiveAsync starting."); + //output.WriteLine("TestEcho: ReceiveAsync starting."); WebSocketReceiveResult recvRet = await cws.ReceiveAsync(receiveSegment, cts.Token); - output.WriteLine("TestEcho: ReceiveAsync done."); + //output.WriteLine("TestEcho: ReceiveAsync done."); Assert.Equal(WebSocketState.Open, cws.State); Assert.Equal(message.Length, recvRet.Count); Assert.Equal(type, recvRet.MessageType); @@ -52,14 +52,14 @@ public static async Task TestEcho( var recvSegment = new ArraySegment(receiveSegment.Array, receiveSegment.Offset, recvRet.Count); Assert.Equal(message, WebSocketData.GetTextFromBuffer(recvSegment)); - output.WriteLine("TestEcho: CloseAsync starting."); + //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."); + //output.WriteLine("TestEcho: CloseAsync done."); Assert.Equal(WebSocketState.Closed, cws.State); Assert.Equal(WebSocketCloseStatus.NormalClosure, cws.CloseStatus); Assert.Equal(closeMessage, cws.CloseStatusDescription); @@ -108,7 +108,7 @@ public static Task GetConnectedWebSocket( using (var cts = new CancellationTokenSource(timeOutMilliseconds)) { - output.WriteLine("GetConnectedWebSocket: ConnectAsync starting."); + //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) || @@ -117,7 +117,7 @@ public static Task GetConnectedWebSocket( (cws.State == WebSocketState.Aborted), "State immediately after ConnectAsync incorrect: " + cws.State); await taskConnect; - output.WriteLine("GetConnectedWebSocket: ConnectAsync done."); + //output.WriteLine("GetConnectedWebSocket: ConnectAsync done."); Assert.Equal(WebSocketState.Open, cws.State); } return cws; From b258ef48f841dfbf22bf4b8cf9b6f3464749a13c Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Tue, 1 Jul 2025 14:00:00 +0100 Subject: [PATCH 12/20] Add missing read-write tests, remove debug logging --- .../Net/Http/Http2LoopbackConnection.cs | 18 +-- .../Helpers/WebSocketEchoHelper.cs | 49 +------- .../tests/AbortTest.Loopback.cs | 4 +- .../tests/ClientWebSocketOptionsTests.cs | 5 +- .../tests/ClientWebSocketTestBase.cs | 109 +++++------------- .../tests/CloseTest.Loopback.cs | 8 +- .../tests/CloseTest.cs | 28 +++-- .../tests/ConnectTest.cs | 16 +-- .../tests/KeepAliveTest.Loopback.cs | 2 +- .../LoopbackServer/Http2LoopbackStream.cs | 20 +--- .../LoopbackWebSocketServer.Echo.cs | 38 ++---- .../LoopbackWebSocketServer.Http.cs | 56 +++------ .../LoopbackServer/LoopbackWebSocketServer.cs | 55 +-------- .../WebSocketHandshakeHelper.cs | 13 +-- .../tests/SendReceiveTest.Loopback.cs | 70 ++++++++++- .../tests/SendReceiveTest.cs | 4 +- .../tests/WebSocketHelper.cs | 8 -- 17 files changed, 178 insertions(+), 325 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs index 8ab96ae0ea7310..0b3dae175766f3 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs @@ -30,7 +30,6 @@ public class Http2LoopbackConnection : GenericLoopbackConnection private bool _expectClientDisconnect; private readonly SemaphoreSlim? _readLock; private readonly SemaphoreSlim? _writeLock; - private readonly TextWriter? _logger; private readonly byte[] _prefix = new byte[24]; public string PrefixString => Encoding.UTF8.GetString(_prefix, 0, _prefix.Length); @@ -44,31 +43,34 @@ private Http2LoopbackConnection(SocketWrapper socket, Stream stream, TimeSpan ti _connectionStream = stream; _timeout = timeout; _transparentPingResponse = httpOptions.EnableTransparentPingResponse; - _logger = httpOptions.Logger; if (httpOptions.EnsureThreadSafeIO) { _readLock = new SemaphoreSlim(1, 1); _writeLock = new SemaphoreSlim(1, 1); + _connectionStream = CreateConcurrentConnectionStream(stream, _readLock, _writeLock); + } - _connectionStream = new DelegateStream( + 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); + await readLock.WaitAsync(cancellationToken); try { return await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); } finally { - _readLock.Release(); + readLock.Release(); } }, writeAsyncFunc: async (buffer, offset, count, cancellationToken) => { - await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + await writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); @@ -76,16 +78,14 @@ private Http2LoopbackConnection(SocketWrapper socket, Stream stream, TimeSpan ti } finally { - _writeLock.Release(); + writeLock.Release(); } }, disposeFunc: (disposing) => { if (disposing) { - // _logger?.WriteLine($"[Http2LoopbackConnection] Disposing HTTP/2 connection"); stream.Dispose(); - // _logger?.WriteLine($"[Http2LoopbackConnection] Disposed"); } } ); diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs index 24c80b9b1c673e..3d4fd19bb9cf01 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs @@ -2,10 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Diagnostics; using System.Net.WebSockets; using System.Text; -using System.IO; using System.Threading; using System.Threading.Tasks; @@ -13,26 +11,20 @@ namespace System.Net.Test.Common { public static class WebSocketEchoHelper { - public static async Task RunEchoAll(WebSocket socket, bool replyWithPartialMessages, bool replyWithEnhancedCloseMessage, TextWriter? logger = null, CancellationToken cancellationToken = default) + 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]; - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] State={socket.State}, starting receive loop..."); - // Stay in loop while websocket is open while (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseSent) { - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] State={socket.State}, waiting for messages..."); var receiveResult = await socket.ReceiveAsync(new ArraySegment(receiveBuffer), cancellationToken); - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Received {receiveResult.MessageType} frame, Count={receiveResult.Count}, EOM={receiveResult.EndOfMessage}"); - if (receiveResult.MessageType == WebSocketMessageType.Close) { - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] CloseStatus={receiveResult.CloseStatus}, CloseStatusDescription={receiveResult.CloseStatusDescription}"); if (receiveResult.CloseStatus == WebSocketCloseStatus.Empty) { await socket.CloseAsync(WebSocketCloseStatus.Empty, null, cancellationToken); @@ -47,7 +39,6 @@ await socket.CloseAsync( receiveResult.CloseStatusDescription, cancellationToken); } - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Closed"); continue; } @@ -56,7 +47,6 @@ await socket.CloseAsync( int offset = receiveResult.Count; while (receiveResult.EndOfMessage == false) { - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] EOM=false, waiting for continuation (offset={offset})..."); if (offset < MaxBufferSize) { receiveResult = await socket.ReceiveAsync( @@ -76,12 +66,10 @@ await socket.CloseAsync( // Close socket if the message was too big. if (offset > MaxBufferSize) { - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Bad message, closing (MessageTooBig)"); await socket.CloseAsync( WebSocketCloseStatus.MessageTooBig, string.Format("{0}: {1} > {2}", WebSocketCloseStatus.MessageTooBig.ToString(), offset, MaxBufferSize), cancellationToken); - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Closed"); continue; } @@ -93,31 +81,22 @@ await socket.CloseAsync( receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, offset); if (receivedMessage == ".close") { - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Requested: Close..."); await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Closed"); } else if (receivedMessage == ".shutdown") { - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Requested: Shutdown writes..."); await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Writes closed"); } else if (receivedMessage == ".abort") { - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Requested: Abort..."); socket.Abort(); - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Aborted"); } else if (receivedMessage == ".delay5sec") { - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Requested: Delay 5s..."); await Task.Delay(5000); - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Wait completed."); } else if (receivedMessage == ".receiveMessageAfterClose") { - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Requested: Echo timestamp and close..."); string message = $"{receivedMessage} {DateTime.Now:HH:mm:ss}"; byte[] buffer = Encoding.UTF8.GetBytes(message); await socket.SendAsync( @@ -125,50 +104,38 @@ await socket.SendAsync( WebSocketMessageType.Text, true, cancellationToken); - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Sent: '{message}', closing..."); await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Closed"); } else if (socket.State == WebSocketState.Open) { - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Text message: '{receivedMessage}'"); sendMessage = true; } } else { - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Binary message (size='{offset}')"); sendMessage = true; } if (sendMessage) { - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Echo..."); await socket.SendAsync( new ArraySegment(receiveBuffer, 0, offset), receiveResult.MessageType, !replyWithPartialMessages, cancellationToken); - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Sent"); } if (receivedMessage == ".closeafter") { - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Requested: Close after echo..."); await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Closed"); } else if (receivedMessage == ".shutdownafter") { - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Requested: Shutdown writes after echo..."); await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] Writes closed"); } } - - //logger?.WriteLine($"[Server - {nameof(RunEchoAll)}] State={socket.State}, Receive loop completed."); } - public static async Task RunEchoHeaders(WebSocket socket, IEnumerable> headers, TextWriter? logger = null, CancellationToken cancellationToken = default) + public static async Task RunEchoHeaders(WebSocket socket, IEnumerable> headers, CancellationToken cancellationToken = default) { const int MaxBufferSize = 1024; var receiveBuffer = new byte[MaxBufferSize]; @@ -184,21 +151,15 @@ public static async Task RunEchoHeaders(WebSocket socket, IEnumerable(sendBuffer), WebSocketMessageType.Text, true, cancellationToken); - //logger?.WriteLine($"[Server - {nameof(RunEchoHeaders)}] State={socket.State}, starting receive loop..."); - // Stay in loop while websocket is open while (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseSent) { - logger?.WriteLine($"[Server - {nameof(RunEchoHeaders)}] State={socket.State}, waiting for messages..."); var receiveResult = await socket.ReceiveAsync(new ArraySegment(receiveBuffer), cancellationToken); if (receiveResult.MessageType == WebSocketMessageType.Close) { - logger?.WriteLine($"[Server - {nameof(RunEchoHeaders)}] Received close frame, CloseStatus={receiveResult.CloseStatus}, CloseStatusDescription={receiveResult.CloseStatusDescription}"); if (receiveResult.CloseStatus == WebSocketCloseStatus.Empty) { await socket.CloseAsync(WebSocketCloseStatus.Empty, null, cancellationToken); @@ -214,17 +175,13 @@ await socket.CloseAsync( continue; } } - - logger?.WriteLine($"[Server - {nameof(RunEchoHeaders)}] State={socket.State}, Receive loop completed."); } - public static async ValueTask ProcessOptions(string queryString, TextWriter? logger = null, CancellationToken cancellationToken = default) + public static async ValueTask ProcessOptions(string queryString, CancellationToken cancellationToken = default) { - //logger?.WriteLine($"[Server - {nameof(ProcessOptions)}] Processing echo options from query string = '{queryString}'"); WebSocketEchoOptions options = WebSocketEchoOptions.Parse(queryString); if (options.Delay is TimeSpan d) { - //logger?.WriteLine($"[Server - {nameof(ProcessOptions)}] delay = {d}"); await Task.Delay(d, cancellationToken).ConfigureAwait(false); } return options; 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 85a459a8437801..0b2d1c08c74b34 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs @@ -87,7 +87,7 @@ public Task AbortClient_ServerGetsCorrectException(AbortType abortType, bool use UseSsl = useSsl, HttpInvoker = GetInvoker(), DisposeServerWebSocket = true, - TestOutputHelper = _output + Output = _output }, timeoutCts.Token); } @@ -111,7 +111,7 @@ public Task ServerPrematureEos_ClientGetsCorrectException(ServerEosType serverEo UseSsl = useSsl, HttpInvoker = null, SkipServerHandshakeResponse = true, - TestOutputHelper = _output + Output = _output }; var serverReceivedEosTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs index 84cb90d9b36908..41ac96104237c4 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs @@ -7,7 +7,7 @@ using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; - +using Microsoft.DotNet.XUnitExtensions; using Xunit; using Xunit.Abstractions; @@ -70,8 +70,7 @@ 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}"); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs index 58b67c03bb1665..17964d85aa283e 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs @@ -3,16 +3,12 @@ using System.Collections.Generic; using System.Diagnostics; -using System.IO; using System.Linq; using System.Net.Http; -using System.Net.Sockets; -using System.Net.Test.Common; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using Microsoft.DotNet.XUnitExtensions; -using TestUtilities; +using System.Net.Test.Common; using Xunit; using Xunit.Abstractions; @@ -25,8 +21,8 @@ public class ClientWebSocketTestBase 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 } + [ o[0], false ], + [ o[0], true ] }).ToArray(); public static readonly bool[] Bool_Values = [ false, true ]; @@ -130,7 +126,6 @@ protected static async Task ReceiveEntireMessageAsync(We protected bool UseSharedHandler => !UseCustomInvoker && !UseHttpClient; protected Action? ConfigureCustomHandler; - protected bool UseSocketsHttpHandler = true;//false; internal virtual Version HttpVersion => Net.HttpVersion.Version11; @@ -141,68 +136,13 @@ protected static async Task ReceiveEntireMessageAsync(We return null; } - HttpMessageHandler handler; - - if (UseSocketsHttpHandler && SocketsHttpHandler.IsSupported) - { - if (ConfigureCustomHandler is not null) - { - throw new InvalidOperationException("ConfigureCustomHandler is not supported when UseSocketsHttpHandler is true."); - } - - var shh = new SocketsHttpHandler(); - shh.SslOptions.RemoteCertificateValidationCallback = (_, _, _, _) => true; - shh.ConnectCallback = async (context, ct) => - { - // Create and connect a socket using default settings. - Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp) { NoDelay = true }; - try - { - await socket.ConnectAsync(context.DnsEndPoint, ct).ConfigureAwait(false); - Stream stream = new NetworkStream(socket, ownsSocket: true); -/*#if !TARGET_BROWSER - var networkStream = stream; - var delegatingStream = new DelegateDelegatingStream(networkStream); - delegatingStream.DisposeFunc = disposing => - { - //_output.WriteLine($"[Client] NetworkStream.Dispose({disposing})"); - if (disposing) - { - networkStream.Dispose(); - } - }; - delegatingStream.DisposeAsyncFunc = () => - { - //_output.WriteLine($"[Client] NetworkStream.DisposeAsync()"); - return networkStream.DisposeAsync(); - }; - stream = delegatingStream; -#endif*/ - return stream; - } - catch - { - socket.Dispose(); - throw; - } - }; - handler = shh; - } - else if (PlatformDetection.IsNotBrowser) - { - var httpClientHandler = new HttpClientHandler - { - ServerCertificateCustomValidationCallback = (_, _, _, _) => true - }; - ConfigureCustomHandler?.Invoke(httpClientHandler); - handler = httpClientHandler; - } - else + HttpClientHandler handler = new HttpClientHandler(); + if (PlatformDetection.IsNotBrowser) { - handler = new HttpClientHandler(); + handler.ServerCertificateCustomValidationCallback = (_, _, _, _) => true; + ConfigureCustomHandler?.Invoke(handler); } - if (UseCustomInvoker) { Debug.Assert(!UseHttpClient); @@ -261,8 +201,8 @@ protected void ConfigureHttpVersion(ClientWebSocketOptions options, Uri uri) public static bool WebSocketsSupported { get { return WebSocketHelper.WebSocketsSupported; } } -#if !TARGET_BROWSER - // Loopback server related functions +#if !TARGET_BROWSER // Loopback server related functions + protected virtual bool SkipIfUseSsl => false; protected Task RunEchoAsync(Func clientFunc, bool useSsl) @@ -271,7 +211,20 @@ protected Task RunEchoAsync(Func clientFunc, bool useSsl) { throw new SkipTestException("SSL is not supported in this test."); } - return LoopbackWebSocketServer.RunEchoAsync(clientFunc, HttpVersion, useSsl, TimeOutMilliseconds, _output); + + var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); + var options = new LoopbackWebSocketServer.Options + { + HttpVersion = HttpVersion, + UseSsl = useSsl, + SkipServerHandshakeResponse = true, + IgnoreServerErrors = true, + AbortServerOnClientExit = true, + ParseEchoOptions = true, + Output = _output + }; + + return LoopbackWebSocketServer.RunEchoAsync(clientFunc, options, timeoutCts.Token); } protected Task RunEchoHeadersAsync(Func clientFunc, bool useSsl) @@ -288,29 +241,19 @@ protected Task RunEchoHeadersAsync(Func clientFunc, bool useSsl) UseSsl = useSsl, IgnoreServerErrors = true, AbortServerOnClientExit = true, - TestOutputHelper = _output + Output = _output }; return LoopbackWebSocketServer.RunAsync( clientFunc, async (requestData, token) => { - // _output?.WriteLine($"[Server - {nameof(RunEchoHeadersAsync)}] WebSocket.CreateFromStream"); var serverWebSocket = WebSocket.CreateFromStream( requestData.TransportStream, new WebSocketCreationOptions { IsServer = true }); - using (var registration = token.Register(() => { - // _output?.WriteLine($"[Server - {nameof(RunEchoHeadersAsync)}] Aborting server WebSocket on cancellation"); - serverWebSocket.Abort(); - })) - { - // _output?.WriteLine($"[Server - {nameof(RunEchoHeadersAsync)}] RunEchoHeaders"); - await WebSocketEchoHelper.RunEchoHeaders(serverWebSocket, requestData.Headers, options.Logger, token); - // _output?.WriteLine($"[Server - {nameof(RunEchoHeadersAsync)}] RunEchoHeaders completed"); - } - - // _output?.WriteLine($"[Server - Run Echo Headers WS] Completed; Server WebSocket state: {serverWebSocket.State}"); + 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/CloseTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs index f7f74d2b5a0a1a..3456c2913c9a92 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs @@ -17,7 +17,7 @@ namespace System.Net.WebSockets.Client.Tests [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets are not supported on browser")] public abstract class CloseTest_Loopback(ITestOutputHelper output) : CloseTestBase(output) { - [Theory, MemberData(nameof(UseSslAndBoolean))] // to move to loopback + [Theory, MemberData(nameof(UseSslAndBoolean))] public Task CloseAsync_ServerInitiatedClose_Success(bool useSsl, bool useCloseOutputAsync) => RunEchoAsync( server => RunClient_CloseAsync_ServerInitiatedClose_Success(server, useCloseOutputAsync), useSsl); @@ -53,15 +53,15 @@ public Task CloseAsync_CloseOutputAsync_Throws(bool useSsl) => RunEchoAsync( public Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(bool useSsl) => RunEchoAsync( RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanClose, useSsl); - [Theory, MemberData(nameof(UseSslAndBoolean))]// to move to loopback + [Theory, MemberData(nameof(UseSslAndBoolean))] public Task CloseOutputAsync_ServerInitiated_CanReceive(bool useSsl, bool delayReceiving) => RunEchoAsync( server => RunClient_CloseOutputAsync_ServerInitiated_CanReceive(server, delayReceiving), useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))]// to move to loopback + [Theory, MemberData(nameof(UseSsl_MemberData))] public Task CloseOutputAsync_ServerInitiated_CanSend(bool useSsl) => RunEchoAsync( RunClient_CloseOutputAsync_ServerInitiated_CanSend, useSsl); - [Theory, MemberData(nameof(UseSslAndBoolean))]// to move to loopback + [Theory, MemberData(nameof(UseSslAndBoolean))] public Task CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(bool useSsl, bool syncState) => RunEchoAsync( server => RunClient_CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(server, syncState), useSsl); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs index e6d19cc28e6782..f0cc28d86ce486 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.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.Text; using System.Threading; using System.Threading.Tasks; @@ -23,18 +21,14 @@ protected async Task RunClient_CloseAsync_ServerInitiatedClose_Success(Uri serve { var cts = new CancellationTokenSource(TimeOutMilliseconds); - //_output.WriteLine("SendAsync starting."); await cws.SendAsync( WebSocketData.GetBufferFromText(shutdownWebSocketMetaCommand), 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); @@ -47,12 +41,10 @@ await cws.SendAsync( Assert.Equal(shutdownWebSocketMetaCommand, 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 @@ -433,6 +425,11 @@ protected async Task RunClient_CloseAsync_DuringConcurrentReceiveAsync_ExpectedS [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] public abstract class CloseTest_External(ITestOutputHelper output) : CloseTestBase(output) { + [ConditionalTheory(nameof(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); + [Theory, MemberData(nameof(EchoServers))] public Task CloseAsync_ClientInitiatedClose_Success(Uri server) => RunClient_CloseAsync_ClientInitiatedClose_Success(server); @@ -465,6 +462,21 @@ public Task CloseAsync_CloseOutputAsync_Throws(Uri server) public Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(Uri server) => RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanClose(server); + [ConditionalTheory(nameof(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); + + [ConditionalTheory(nameof(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(nameof(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); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs index e7a115d78df84c..3f28c9797edead 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs @@ -1,11 +1,7 @@ // 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; @@ -84,7 +80,6 @@ protected async Task RunClient_ConnectAsync_CookieHeaders_Success(Uri server) else { ConfigureCustomHandler = handler => handler.CookieContainer = cookies; - UseSocketsHttpHandler = false; } using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) @@ -110,8 +105,6 @@ protected async Task RunClient_ConnectAsync_CookieHeaders_Success(Uri server) Assert.Equal(WebSocketMessageType.Text, recvResult.MessageType); string headers = WebSocketData.GetTextFromBuffer(new ArraySegment(buffer, 0, recvResult.Count)); - // Console.WriteLine(headers); - Assert.Contains("Cookies=Are Yummy", headers); Assert.Contains("Especially=Chocolate Chip", headers); Assert.Equal(server.Scheme == "wss", headers.Contains("Occasionally=Raisin")); @@ -134,7 +127,7 @@ protected async Task RunClient_ConnectAsync_PassNoSubProtocol_ServerRequires_Thr WebSocketException ex = await Assert.ThrowsAsync(() => ConnectAsync(cws, ub.Uri, cts.Token)); _output.WriteLine(ex.Message); - Assert.True(ex.WebSocketErrorCode == WebSocketError.UnsupportedProtocol || // TODO + 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); @@ -172,13 +165,14 @@ protected async Task RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClose using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) using (LoopbackProxyServer proxyServer = LoopbackProxyServer.Create()) { - ConfigureCustomHandler = handler => handler.Proxy = new WebProxy(proxyServer.Uri); - UseSocketsHttpHandler = false; - if (UseSharedHandler) { cws.Options.Proxy = new WebProxy(proxyServer.Uri); } + else + { + ConfigureCustomHandler = handler => handler.Proxy = new WebProxy(proxyServer.Uri); + } await ConnectAsync(cws, server, cts.Token); 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 e67557b2225c52..527b0a91df2627 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs @@ -38,7 +38,7 @@ public Task KeepAlive_LongDelayBetweenSendReceives_Succeeds(bool useSsl) clientOptions.KeepAliveTimeout = TimeSpan.FromSeconds(1); }, ConfigureHttp2Options = http2Options => http2Options.EnsureThreadSafeIO = true, - TestOutputHelper = _output + Output = _output }; return LoopbackWebSocketServer.RunAsync( diff --git a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/Http2LoopbackStream.cs b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/Http2LoopbackStream.cs index 5195cf2c901110..b841eead6ea248 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/Http2LoopbackStream.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/Http2LoopbackStream.cs @@ -15,9 +15,7 @@ public class Http2LoopbackStream : Stream private readonly int _streamId; private bool _readEnded; private ReadOnlyMemory _leftoverReadData; - private bool _sendEosOnDispose; private bool _sendResetOnDispose; - private readonly TextWriter? _logger; public override bool CanRead => true; public override bool CanSeek => false; @@ -26,13 +24,11 @@ public class Http2LoopbackStream : Stream public Http2LoopbackConnection Connection => _connection; public int StreamId => _streamId; - public Http2LoopbackStream(Http2LoopbackConnection connection, int streamId, bool sendEosOnDispose = true, bool sendResetOnDispose = true, TextWriter? logger = null) + public Http2LoopbackStream(Http2LoopbackConnection connection, int streamId, bool sendResetOnDispose = true) { _connection = connection; _streamId = streamId; - _sendEosOnDispose = sendEosOnDispose; _sendResetOnDispose = sendResetOnDispose; - _logger = logger; } public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) @@ -47,7 +43,6 @@ public override async ValueTask ReadAsync(Memory buffer, Cancellation if (_readEnded) { - // _logger?.WriteLine($"[Http2LoopbackStream] Reached EOS"); return 0; } @@ -76,19 +71,10 @@ public override async ValueTask DisposeAsync() { try { - //_logger?.WriteLine($"[Http2LoopbackStream] Disposing"); - - // _logger?.WriteLine($"[Http2LoopbackStream] Disposing, _readEnded: {_readEnded}, _sendResetOnDispose: {_sendResetOnDispose}"); - - if (_sendEosOnDispose) - { - // _logger?.WriteLine($"[Http2LoopbackStream] Sending EOS"); - await _connection.SendResponseDataAsync(_streamId, Memory.Empty, endStream: true).ConfigureAwait(false); - } + await _connection.SendResponseDataAsync(_streamId, Memory.Empty, endStream: true).ConfigureAwait(false); if (!_readEnded && _sendResetOnDispose) { - // _logger?.WriteLine($"[Http2LoopbackStream] Sending RST_STREAM..."); var rstFrame = new RstStreamFrame(FrameFlags.None, (int)ProtocolErrors.NO_ERROR, _streamId); await _connection.WriteFrameAsync(rstFrame).ConfigureAwait(false); } @@ -101,8 +87,6 @@ public override async ValueTask DisposeAsync() { // Ignore connection errors } - // _logger?.WriteLine($"[Http2LoopbackStream] Disposed"); - //return default; } public override void Flush() { } 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 index 022fbe76d41701..5257b8f54ef817 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Echo.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Echo.cs @@ -5,64 +5,48 @@ using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace System.Net.WebSockets.Client.Tests { public static partial class LoopbackWebSocketServer { - public static Task RunEchoAsync(Func loopbackClientFunc, Version httpVersion, bool useSsl, int timeOutMilliseconds, ITestOutputHelper output) + public static Task RunEchoAsync(Func loopbackClientFunc, Options options, CancellationToken cancellationToken) { - var timeoutCts = new CancellationTokenSource(timeOutMilliseconds); - var options = new Options - { - HttpVersion = httpVersion, - UseSsl = useSsl, - SkipServerHandshakeResponse = true, // to negotiate subprotocols and extensions - IgnoreServerErrors = true, - AbortServerOnClientExit = true, - ParseEchoOptions = true, - TestOutputHelper = output - }; - - //output.WriteLine("[Common - Run Echo WS] RunEchoAsync called with options: " + options); + Assert.True(options.IgnoreServerErrors); + Assert.True(options.AbortServerOnClientExit); return RunAsyncPrivate( loopbackClientFunc, (data, token) => RunEchoServerWebSocketAsync(data, options, token), options, - timeoutCts.Token); + cancellationToken); } private static async Task RunEchoServerWebSocketAsync(WebSocketRequestData data, Options options, CancellationToken cancellationToken) { - //options.Logger?.WriteLine($"[Server - Run Echo WS] Starting..."); - - Assert.NotNull(data.EchoOptions); - WebSocketEchoOptions echoOptions = data.EchoOptions.Value; + 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 }; } - await SendNegotiatedServerResponseAsync(data, options, cancellationToken).ConfigureAwait(false); - - //options.Logger?.WriteLine($"[Server - Run Echo WS] Connection established"); + if (options.SkipServerHandshakeResponse) + { + await SendNegotiatedServerResponseAsync(data, options, cancellationToken).ConfigureAwait(false); + } - var webSocket = await RunServerWebSocketAsync( + await RunServerWebSocketAsync( data, (serverWebSocket, token) => WebSocketEchoHelper.RunEchoAll( serverWebSocket, echoOptions.ReplyWithPartialMessages, echoOptions.ReplyWithEnhancedCloseMessage, - options.Logger, token), options, cancellationToken).ConfigureAwait(false); - - //options.Logger?.WriteLine($"[Server - Run Echo WS] Completed; Server WebSocket state: {webSocket.State}"); } } } 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 index 080cb5e2e5d575..d2239f9d61ac2a 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs @@ -19,8 +19,6 @@ private static Task RunClientAndServerAsync( CancellationToken clientExitCt, CancellationToken globalCt) { - //options.Logger?.WriteLine($"[Common - {nameof(RunClientAndServerAsync)}] HTTP version: {options.HttpVersion}"); - if (options.HttpVersion == HttpVersion.Version11) { return LoopbackServer.CreateClientAndServerAsync( @@ -32,7 +30,7 @@ private static Task RunClientAndServerAsync( if (options.HttpVersion == HttpVersion.Version20) { - var http2Options = new Http2Options { WebSocketEndpoint = true, UseSsl = options.UseSsl, EnsureThreadSafeIO = true, Logger = options.Logger }; + var http2Options = new Http2Options { WebSocketEndpoint = true, UseSsl = options.UseSsl, EnsureThreadSafeIO = true }; options.ConfigureHttp2Options?.Invoke(http2Options); return Http2LoopbackServer.CreateClientAndServerAsync( @@ -53,17 +51,13 @@ private static Task ProcessHttp11WebSocketRequest( => http11server.AcceptConnectionAsync( async connection => { - //options.Logger?.WriteLine("[Server - Process HTTP/1.1] Processing handshake"); var requestData = await WebSocketHandshakeHelper.ProcessHttp11RequestAsync( connection, options.SkipServerHandshakeResponse, options.ParseEchoOptions, - options.Logger, cancellationToken).ConfigureAwait(false); - //options.Logger?.WriteLine("[Server - Process HTTP/1.1] Running server callback"); await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); - //options.Logger?.WriteLine("[Server - Process HTTP/1.1] Completed server callback"); }); private static async Task ProcessHttp2WebSocketRequest( @@ -72,46 +66,40 @@ private static async Task ProcessHttp2WebSocketRequest( Options options, CancellationToken cancellationToken) { - //options.Logger?.WriteLine("[Server - Process HTTP/2] Processing handshake"); var requestData = await WebSocketHandshakeHelper.ProcessHttp2RequestAsync( http2Server, options.SkipServerHandshakeResponse, options.ParseEchoOptions, - sendEosOnDispose: true,//!options.AbortServerOnClientExit, - options.Logger, cancellationToken).ConfigureAwait(false); - //options.Logger?.WriteLine("[Server - Process HTTP/2] Running server callback"); await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); - //options.Logger?.WriteLine("[Server - Process HTTP/2] Completed server callback"); + var http2Connection = requestData.Http2Connection!; - if (options.AbortServerOnClientExit) + if (options.AbortServerOnClientExit) // we need to wait for the client to disconnect { - //options.Logger?.WriteLine("[Server - Process HTTP/2] Waiting for the client to exit..."); - var h2conn = requestData.Http2Connection!; - - - Frame frame = await h2conn.ReadFrameAsync(cancellationToken).ConfigureAwait(false); - if (frame is not null) + // 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) { - bool isRstStream = frame.Type == FrameType.RstStream; - bool isLastAck = frame.Type == FrameType.Data && ((DataFrame)frame).AckFlag; - if (!isRstStream && !isLastAck) + var frame = await http2Connection.ReadFrameAsync(cancellationToken).ConfigureAwait(false); + if (frame is null) + { + // No more frames to read + break; + } + + if (!options.IgnoreServerErrors) { - options.Logger?.WriteLine($"[Server - Process HTTP/2] Drained frame: {frame}"); + Assert.False(frame.Type == FrameType.Data && !((DataFrame)frame).EndStreamFlag, $"Unexpected DATA frame: {frame}"); } } - // Wait for the client to exit - await h2conn.WaitForConnectionShutdownAsync(ignoreUnexpectedFrames: true).ConfigureAwait(false); + await http2Connection.WaitForConnectionShutdownAsync(options.IgnoreServerErrors).ConfigureAwait(false); } else { - //options.Logger?.WriteLine("[Server - Process HTTP/2] Send GOAWAY and shutdown..."); - // This will send GOAWAY - await requestData.Http2Connection!.ShutdownIgnoringErrorsAsync(requestData.Http2StreamId.Value).ConfigureAwait(false); + await http2Connection.ShutdownIgnoringErrorsAsync(requestData.Http2StreamId.Value).ConfigureAwait(false); } - //options.Logger?.WriteLine("[Server - Process HTTP/2] Shutdown completed"); } private static async Task RunHttpServer( @@ -125,19 +113,14 @@ private static async Task RunHttpServer( { try { - //options.Logger?.WriteLine("[Server - HTTP (generic)] Starting HTTP server..."); using CancellationTokenSource linkedCts = CancellationTokenSource.CreateLinkedTokenSource(globalCt, clientExitCt); await httpServerFunc(httpServer, wsServerFunc, options, linkedCts.Token) .WaitAsync(linkedCts.Token).ConfigureAwait(false); - - //options.Logger?.WriteLine("[Server - HTTP (generic)] Completed successfully."); } catch (Exception e) when (options.IgnoreServerErrors) { - //options.Logger?.WriteLine($"[Server - HTTP (generic)] Completed via IgnoreServerErrors ({e.GetType().Name})"); - if (e is OperationCanceledException && clientExitCt.IsCancellationRequested) { return; // expected for aborting on client exit @@ -156,12 +139,11 @@ await httpServerFunc(httpServer, wsServerFunc, options, linkedCts.Token) const string closeOnAbortedMsg = "The WebSocket is in an invalid state ('Aborted') for this operation. Valid states are: 'Open, CloseSent, CloseReceived'"; if (we.Message == closeOnClosedMsg || we.Message == closeOnAbortedMsg) { - return; // expected, see https://github.com/dotnet/runtime/issues/22000 + return; // expected (Close on a closed WebSocket is not no-op: see https://github.com/dotnet/runtime/issues/22000) } } - Console.WriteLine($"[WARN] Server aborted on a WebSocketException ({we.WebSocketErrorCode}): {we.Message}"); - //options.Logger?.WriteLine($"[Server - HTTP (generic)][WARN] Server aborted on a WebSocketException ({we.WebSocketErrorCode}): {we.Message}"); + options.Output?.WriteLine($"[WARN] Server aborted on a WebSocketException ({we.WebSocketErrorCode}): {we}"); return; // ignore } 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 af741db07d7ab4..df7d7e5fcd70fe 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.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.Diagnostics; using System.IO; using System.Net.Http; using System.Net.Test.Common; @@ -52,32 +51,21 @@ private static Task RunAsyncPrivate( { if (!options.AbortServerOnClientExit) { - //options.Logger?.WriteLine("[Common - RunAsyncPrivate] Starting independent client and server"); return RunClientAndServerAsync( loopbackClientFunc, loopbackServerFunc, options, CancellationToken.None, globalCt); } CancellationTokenSource clientExitCts = new CancellationTokenSource(); - //options.Logger?.WriteLine("[Common - RunAsyncPrivate] Starting client and server"); - return RunClientAndServerAsync( async uri => { try { - //options.Logger?.WriteLine("[Client] starting client function"); await loopbackClientFunc(uri); - //options.Logger?.WriteLine("[Client] client function completed successfully"); } - //catch (Exception ex) - //{ - //options.Logger?.WriteLine($"[Client] client function failed with {ex}"); - // throw; - //} finally { - //options.Logger?.WriteLine("[Client] cancelling client exit token"); clientExitCts.Cancel(); } }, @@ -87,40 +75,24 @@ private static Task RunAsyncPrivate( globalCt); } - private static async Task RunServerWebSocketAsync( + private static async Task RunServerWebSocketAsync( WebSocketRequestData requestData, Func serverWebSocketFunc, Options options, CancellationToken cancellationToken) { - //options.Logger?.WriteLine("[Server - Run WS] Starting..."); - var wsOptions = new WebSocketCreationOptions { IsServer = true, SubProtocol = options.ServerSubProtocol }; var serverWebSocket = WebSocket.CreateFromStream(requestData.TransportStream, wsOptions); - using (var registration = cancellationToken.Register(() => - { - //options.Logger?.WriteLine("[Server - Run WS] Aborting server on cancellation"); - serverWebSocket.Abort(); - })) + using (var registration = cancellationToken.Register(serverWebSocket.Abort)) { - //options.Logger?.WriteLine("[Server - Run WS] Starting server WS callback"); await serverWebSocketFunc(serverWebSocket, cancellationToken).ConfigureAwait(false); - //options.Logger?.WriteLine("[Server - Run WS] server WS callback completed"); } if (options.DisposeServerWebSocket) { - //options.Logger?.WriteLine("[Server - Run WS] Disposing WebSocket"); serverWebSocket.Dispose(); } - else - { - //options.Logger?.WriteLine("[Server - Run WS] Not disposing WebSocket. State: " + serverWebSocket.State); - } - - //options.Logger?.WriteLine("[Server - Run WS] Completed"); - return serverWebSocket; } private static async Task RunClientWebSocketAsync( @@ -181,28 +153,7 @@ public record class Options() public bool DisposeHttpInvoker { get; set; } public Action? ConfigureClientOptions { get; set; } - private TestOutputWriter? _testOutputWriter; - public TextWriter? Logger => _testOutputWriter; - public ITestOutputHelper? TestOutputHelper { - get => _testOutputWriter; - set { _testOutputWriter = TestOutputWriter.Convert(value); } - } - - private class TestOutputWriter(ITestOutputHelper inner) : TextWriter, ITestOutputHelper - { - public override Encoding Encoding => Encoding.Unicode; - public override void Write(char value) => throw new NotSupportedException("Use WriteLine instead"); - public override void Write(char[] buffer, int index, int count) => throw new NotSupportedException("Use WriteLine instead"); - public override void WriteLine(string value) => inner.WriteLine(value); - public override void WriteLine(string format, params object[] args) => inner.WriteLine(format, args); - void ITestOutputHelper.WriteLine(string message) => inner.WriteLine(message); - void ITestOutputHelper.WriteLine(string format, params object[] args) => inner.WriteLine(format, args); - - public static TestOutputWriter? Convert(ITestOutputHelper? output) - => output is TestOutputWriter tow ? tow - : output is null ? null - : new TestOutputWriter(output); - } + public ITestOutputHelper? Output { 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 d3a9ae253a7684..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,12 +2,9 @@ // 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; -using System.Text; -using System.IO; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -20,7 +17,6 @@ public static async Task ProcessHttp11RequestAsync( LoopbackServer.Connection connection, bool skipServerHandshakeResponse = false, bool parseEchoOptions = false, - TextWriter? logger = null, CancellationToken cancellationToken = default) { List headers = await connection.ReadRequestHeaderAsync().WaitAsync(cancellationToken).ConfigureAwait(false); @@ -44,7 +40,7 @@ public static async Task ProcessHttp11RequestAsync( // 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, logger).ConfigureAwait(false); + data.EchoOptions = await WebSocketEchoHelper.ProcessOptions(query, cancellationToken).ConfigureAwait(false); } else { @@ -98,8 +94,6 @@ public static async Task ProcessHttp2RequestAsync( Http2LoopbackServer server, bool skipServerHandshakeResponse = false, bool parseEchoOptions = false, - bool sendEosOnDispose = true, - TextWriter? logger = null, CancellationToken cancellationToken = default) { var connection = await server.EstablishConnectionAsync(new SettingsEntry { SettingId = SettingId.EnableConnect, Value = 1 }) @@ -137,7 +131,7 @@ public static async Task ProcessHttp2RequestAsync( { // 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, logger).ConfigureAwait(false); + data.EchoOptions = await WebSocketEchoHelper.ProcessOptions(query, cancellationToken).ConfigureAwait(false); } else { @@ -158,8 +152,7 @@ public static async Task ProcessHttp2RequestAsync( await SendHttp2ServerResponseAsync(connection, streamId, cancellationToken: cancellationToken).ConfigureAwait(false); } - // logger?.WriteLine($"[Server - HTTP/2 WS Handshake] Created Http2LoopbackStream for streamId {streamId}, sendEosOnDispose: {sendEosOnDispose}"); - data.TransportStream = new Http2LoopbackStream(connection, streamId, sendEosOnDispose: sendEosOnDispose, sendResetOnDispose: false, logger); + data.TransportStream = new Http2LoopbackStream(connection, streamId, sendResetOnDispose: false); return data; } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs index 258daed0b87f64..999e18f2f7ebd6 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs @@ -56,9 +56,10 @@ public Task ZeroByteReceive_CompletesWhenDataAvailable(Uri server) => RunClient_ZeroByteReceive_CompletesWhenDataAvailable(server); } - public class MemorySendReceiveTest(ITestOutputHelper output) : SendReceiveTest(output) - { - protected override async Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) +#region Memory send/receive tests + public abstract class MemorySendReceiveTest_Loopback(ITestOutputHelper output) : SendReceiveTest_Loopback(output) + { + protected override async Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) { ValueWebSocketReceiveResult r = await ws.ReceiveAsync( (Memory)arraySegment, @@ -74,7 +75,38 @@ protected override Task SendAsync(WebSocket ws, ArraySegment arraySegment, cancellationToken).AsTask(); } - public class ArraySegmentSendReceiveTest(ITestOutputHelper output) : SendReceiveTest(output) + public sealed class MemorySendReceiveTest_SharedHandler_Loopback(ITestOutputHelper output) : MemorySendReceiveTest_Loopback(output) { } + + public sealed class MemorySendReceiveTest_Invoker_Loopback(ITestOutputHelper output) : MemorySendReceiveTest_Loopback(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class MemorySendReceiveTest_HttpClient_Loopback(ITestOutputHelper output) : MemorySendReceiveTest_Loopback(output) + { + protected override bool UseHttpClient => true; + } + + // --- HTTP/2 WebSocket loopback tests --- + + public abstract class MemorySendReceiveTest_Http2Loopback(ITestOutputHelper output) : MemorySendReceiveTest_Loopback(output) + { + internal override Version HttpVersion => Net.HttpVersion.Version20; + } + + public sealed class MemorySendReceiveTest_Invoker_Http2Loopback(ITestOutputHelper output) : MemorySendReceiveTest_Http2Loopback(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class MemorySendReceiveTest_HttpClient_Http2Loopback(ITestOutputHelper output) : MemorySendReceiveTest_Http2Loopback(output) + { + protected override bool UseHttpClient => true; + } +#endregion + +#region ArraySegment send/receive tests + public abstract class ArraySegmentSendReceiveTest(ITestOutputHelper output) : SendReceiveTest_Loopback(output) { protected override Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) => ws.ReceiveAsync(arraySegment, cancellationToken); @@ -82,4 +114,34 @@ protected override Task ReceiveAsync(WebSocket ws, Array protected override Task SendAsync(WebSocket ws, ArraySegment arraySegment, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) => ws.SendAsync(arraySegment, messageType, endOfMessage, cancellationToken); } + + public sealed class ArraySegmentSendReceiveTest_SharedHandler_Loopback(ITestOutputHelper output) : ArraySegmentSendReceiveTest_Loopback(output) { } + + public sealed class ArraySegmentSendReceiveTest_Invoker_Loopback(ITestOutputHelper output) : ArraySegmentSendReceiveTest_Loopback(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class ArraySegmentSendReceiveTest_HttpClient_Loopback(ITestOutputHelper output) : ArraySegmentSendReceiveTest_Loopback(output) + { + protected override bool UseHttpClient => true; + } + + // --- HTTP/2 WebSocket loopback tests --- + + public abstract class ArraySegmentSendReceiveTest_Http2Loopback(ITestOutputHelper output) : ArraySegmentSendReceiveTest_Loopback(output) + { + internal override Version HttpVersion => Net.HttpVersion.Version20; + } + + public sealed class ArraySegmentSendReceiveTest_Invoker_Http2Loopback(ITestOutputHelper output) : ArraySegmentSendReceiveTest_Http2Loopback(output) + { + protected override bool UseCustomInvoker => true; + } + + public sealed class ArraySegmentSendReceiveTest_HttpClient_Http2Loopback(ITestOutputHelper output) : ArraySegmentSendReceiveTest_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 c5ae5180dc2a1f..cc04e984281d19 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs @@ -531,10 +531,10 @@ public Task SendReceive_VaryingLengthBuffers_Success(Uri server) public Task SendReceive_Concurrent_Success(Uri server) => RunClient_SendReceive_Concurrent_Success(server); - /*[ActiveIssue("https://github.com/dotnet/runtime/issues/54153", TestPlatforms.Browser)] + [ActiveIssue("https://github.com/dotnet/runtime/issues/54153", TestPlatforms.Browser)] [Fact] public Task SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated() - => RunClient_SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated();*/ + => RunClient_SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated(); [Theory, MemberData(nameof(EchoServers))] public Task ZeroByteReceive_CompletesWhenDataAvailable(Uri server) diff --git a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs index c9181378298bf7..b6fe5c88d2df8c 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs @@ -34,14 +34,10 @@ public static async Task TestEcho( using (ClientWebSocket cws = await GetConnectedWebSocket(server, timeOutMilliseconds, output, configureOptions, 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); @@ -52,14 +48,12 @@ public static async Task TestEcho( 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); @@ -108,7 +102,6 @@ public static Task GetConnectedWebSocket( 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) || @@ -117,7 +110,6 @@ public static Task GetConnectedWebSocket( (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; From 8631646fcff1beb27a1d29477f98aba7c3faa2d1 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Wed, 2 Jul 2025 23:19:41 +0100 Subject: [PATCH 13/20] Fixed and unified all tests with exception of send-receive --- .../System/Net/Configuration.WebSockets.cs | 38 +-- .../Helpers/WebSocketEchoHelper.cs | 25 +- .../Helpers/WebSocketEchoOptions.cs | 23 +- .../tests/AbortTest.Loopback.cs | 59 ++-- .../tests/AbortTest.cs | 47 +++- .../tests/CancelTest.Loopback.cs | 39 +-- .../tests/CancelTest.cs | 49 +++- .../tests/ClientWebSocketOptionsTests.cs | 5 +- .../tests/ClientWebSocketTestBase.Loopback.cs | 56 ++++ .../tests/ClientWebSocketTestBase.cs | 127 ++------- .../tests/CloseTest.Loopback.cs | 58 ++-- .../tests/CloseTest.cs | 69 +++-- .../tests/ConnectTest.Http2.cs | 132 +-------- .../tests/ConnectTest.Invoker.cs | 90 ++++++ .../tests/ConnectTest.Loopback.cs | 148 +++++----- .../tests/ConnectTest.SharedHandler.cs | 38 ++- .../tests/ConnectTest.cs | 121 +++++++- .../tests/DeflateTests.cs | 38 ++- .../tests/KeepAliveTest.Loopback.cs | 47 +++- .../tests/SendReceiveTest.Http2.cs | 17 +- .../tests/SendReceiveTest.Loopback.cs | 197 +++++++------ .../tests/SendReceiveTest.cs | 262 ++++++++---------- .../System.Net.WebSockets.Client.Tests.csproj | 7 +- .../tests/WebSocketHelper.cs | 89 +++--- .../tests/WebSocketCreateTest.cs | 9 +- 25 files changed, 1013 insertions(+), 777 deletions(-) create mode 100644 src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Loopback.cs create mode 100644 src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Invoker.cs diff --git a/src/libraries/Common/tests/System/Net/Configuration.WebSockets.cs b/src/libraries/Common/tests/System/Net/Configuration.WebSockets.cs index c5686be67b4ef9..87ea91a0c07af9 100644 --- a/src/libraries/Common/tests/System/Net/Configuration.WebSockets.cs +++ b/src/libraries/Common/tests/System/Net/Configuration.WebSockets.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Linq; + namespace System.Net.Test.Common { public static partial class Configuration @@ -22,35 +24,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/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs index 3d4fd19bb9cf01..4b1e44b6b223a6 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs @@ -11,6 +11,17 @@ 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; @@ -79,23 +90,23 @@ await socket.CloseAsync( if (receiveResult.MessageType == WebSocketMessageType.Text) { receivedMessage = Encoding.UTF8.GetString(receiveBuffer, 0, offset); - if (receivedMessage == ".close") + if (receivedMessage == EchoControlMessage.Close) { await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); } - else if (receivedMessage == ".shutdown") + else if (receivedMessage == EchoControlMessage.Shutdown) { await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); } - else if (receivedMessage == ".abort") + else if (receivedMessage == EchoControlMessage.Abort) { socket.Abort(); } - else if (receivedMessage == ".delay5sec") + else if (receivedMessage == EchoControlMessage.Delay5Sec) { await Task.Delay(5000); } - else if (receivedMessage == ".receiveMessageAfterClose") + else if (receivedMessage == EchoControlMessage.ReceiveMessageAfterClose) { string message = $"{receivedMessage} {DateTime.Now:HH:mm:ss}"; byte[] buffer = Encoding.UTF8.GetBytes(message); @@ -124,11 +135,11 @@ await socket.SendAsync( !replyWithPartialMessages, cancellationToken); } - if (receivedMessage == ".closeafter") + if (receivedMessage == EchoControlMessage.CloseAfter) { await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); } - else if (receivedMessage == ".shutdownafter") + else if (receivedMessage == EchoControlMessage.ShutdownAfter) { await socket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, receivedMessage, cancellationToken); } diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs index c213d36bc35678..9f8576e50bacea 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs @@ -5,6 +5,15 @@ 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; } @@ -21,8 +30,8 @@ public static WebSocketEchoOptions Parse(string query) return new WebSocketEchoOptions { - ReplyWithPartialMessages = query.Contains("replyWithPartialMessages"), - ReplyWithEnhancedCloseMessage = query.Contains("replyWithEnhancedCloseMessage"), + ReplyWithPartialMessages = query.Contains(EchoQueryKey.ReplyWithPartialMessages), + ReplyWithEnhancedCloseMessage = query.Contains(EchoQueryKey.ReplyWithEnhancedCloseMessage), SubProtocol = ParseSubProtocol(query), Delay = ParseDelay(query) }; @@ -30,23 +39,23 @@ public static WebSocketEchoOptions Parse(string query) private static string ParseSubProtocol(string query) { - const string subProtocolKey = "subprotocol="; + const string subProtocolEquals = $"{EchoQueryKey.SubProtocol}="; - var index = query.IndexOf(subProtocolKey); + var index = query.IndexOf(subProtocolEquals); if (index == -1) { return null; } - var subProtocol = query.Substring(index + subProtocolKey.Length); + var subProtocol = query.Substring(index + subProtocolEquals.Length); return subProtocol.Contains("&") ? subProtocol.Substring(0, subProtocol.IndexOf("&")) : subProtocol; } private static TimeSpan? ParseDelay(string query) - => query.Contains("delay10sec") + => query.Contains(EchoQueryKey.Delay10Sec) ? TimeSpan.FromSeconds(10) - : query.Contains("delay20sec") ? TimeSpan.FromSeconds(20) : null; + : 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 0b2d1c08c74b34..63e6f75b9fbcc1 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs @@ -10,36 +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(ITestOutputHelper output) : AbortTestBase(output) + public abstract class AbortTest_LoopbackBase(ITestOutputHelper output) : AbortTestBase(output) { - // --- Loopback Echo Server "overrides" --- + #region Common (Echo Server) tests - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage(bool useSsl) => RunEchoAsync( RunClient_Abort_ConnectAndAbort_ThrowsWebSocketExceptionWithMessage, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task Abort_SendAndAbort_Success(bool useSsl) => RunEchoAsync( RunClient_Abort_SendAndAbort_Success, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task Abort_ReceiveAndAbort_Success(bool useSsl) => RunEchoAsync( RunClient_Abort_ReceiveAndAbort_Success, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task Abort_CloseAndAbort_Success(bool useSsl) => RunEchoAsync( RunClient_Abort_CloseAndAbort_Success, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task ClientWebSocket_Abort_CloseOutputAsync(bool useSsl) => RunEchoAsync( RunClient_ClientWebSocket_Abort_CloseOutputAsync, useSsl); - // --- + #endregion - public static object[][] AbortClient_MemberData = ToMemberData(Enum.GetValues(), UseSsl_Values, /* verifySendReceive */ Bool_Values); + #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 }; @@ -92,10 +94,10 @@ public Task AbortClient_ServerGetsCorrectException(AbortType abortType, bool use 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 }; @@ -176,8 +178,7 @@ await SendServerResponseAndEosAsync( 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 { @@ -210,9 +211,25 @@ protected static async Task VerifySendReceiveAsync(WebSocket ws, byte[] localMsg await sendTask.ConfigureAwait(false); await remoteAck.WaitAsync(cancellationToken).ConfigureAwait(false); } + + #endregion + } + + 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); } - // --- HTTP/1.1 WebSocket loopback tests --- + public abstract class AbortTest_Http2Loopback(ITestOutputHelper output) : AbortTest_LoopbackBase(output) + { + 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); + } + + #region Runnable test classes: HTTP/1.1 Loopback public sealed class AbortTest_SharedHandler_Loopback(ITestOutputHelper output) : AbortTest_Loopback(output) { } @@ -226,15 +243,9 @@ public sealed class AbortTest_HttpClient_Loopback(ITestOutputHelper output) : Ab protected override bool UseHttpClient => true; } - // --- HTTP/2 WebSocket loopback tests --- + #endregion - public abstract class AbortTest_Http2Loopback(ITestOutputHelper output) : AbortTest_Loopback(output) - { - internal 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); - } + #region Runnable test classes: HTTP/2 Loopback public sealed class AbortTest_Invoker_Http2Loopback(ITestOutputHelper output) : AbortTest_Http2Loopback(output) { @@ -245,4 +256,6 @@ public sealed class AbortTest_HttpClient_Http2Loopback(ITestOutputHelper output) { 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 4ca39ab946f0d8..5603236d331114 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs @@ -7,18 +7,43 @@ 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 { + // + // 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) { + #region Common (Echo Server) tests + 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); @@ -39,7 +64,7 @@ await TestCancellation(async (cws) => var cts = new CancellationTokenSource(TimeOutMilliseconds); Task t = cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, cts.Token); @@ -57,7 +82,7 @@ await TestCancellation(async (cws) => var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -80,7 +105,7 @@ await TestCancellation(async (cws) => var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -102,7 +127,7 @@ await TestCancellation(async (cws) => var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -116,12 +141,16 @@ 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); @@ -141,8 +170,12 @@ public Task Abort_CloseAndAbort_Success(Uri 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) @@ -154,4 +187,6 @@ public sealed class AbortTest_HttpClient_External(ITestOutputHelper output) : Ab { 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 index 5cd8bb063849b7..0e713c566c0d2c 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.Loopback.cs @@ -2,52 +2,58 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Threading.Tasks; -using Microsoft.DotNet.XUnitExtensions; using Xunit; using Xunit.Abstractions; namespace System.Net.WebSockets.Client.Tests { - // --- Loopback Echo Server "overrides" --- - [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) { - [Theory, MemberData(nameof(UseSsl_MemberData))] + #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_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task SendAsync_Cancel_Success(bool useSsl) => RunEchoAsync( RunClient_SendAsync_Cancel_Success, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task ReceiveAsync_Cancel_Success(bool useSsl) => RunEchoAsync( RunClient_ReceiveAsync_Cancel_Success, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task CloseAsync_Cancel_Success(bool useSsl) => RunEchoAsync( RunClient_CloseAsync_Cancel_Success, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task CloseOutputAsync_Cancel_Success(bool useSsl) => RunEchoAsync( RunClient_CloseOutputAsync_Cancel_Success, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task ReceiveAsync_CancelThenReceive_ThrowsOperationCanceledException(bool useSsl) => RunEchoAsync( RunClient_ReceiveAsync_CancelThenReceive_ThrowsOperationCanceledException, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException(bool useSsl) => RunEchoAsync( RunClient_ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException(bool useSsl) => RunEchoAsync( RunClient_ReceiveAsync_AfterCancellationDoReceiveAsync_ThrowsWebSocketException, useSsl); + + #endregion } - // --- HTTP/1.1 WebSocket loopback tests --- + 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) { } @@ -61,12 +67,9 @@ public sealed class CancelTest_HttpClient_Loopback(ITestOutputHelper output) : C protected override bool UseHttpClient => true; } - // --- HTTP/2 WebSocket loopback tests --- + #endregion - public abstract class CancelTest_Http2Loopback(ITestOutputHelper output) : CancelTest_Loopback(output) - { - internal override Version HttpVersion => Net.HttpVersion.Version20; - } + #region Runnable test classes: HTTP/2 Loopback public sealed class CancelTest_Invoker_Http2Loopback(ITestOutputHelper output) : CancelTest_Http2Loopback(output) { @@ -77,4 +80,6 @@ public sealed class CancelTest_HttpClient_Http2Loopback(ITestOutputHelper 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 30027eabf409cf..5954eee8ca4561 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs @@ -7,18 +7,45 @@ 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 { + // + // 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) { + #region Common (Echo Server) tests + 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}"); @@ -31,7 +58,7 @@ await TestCancellation((cws) => { var cts = new CancellationTokenSource(5); return cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, cts.Token); @@ -46,7 +73,7 @@ await TestCancellation(async (cws) => var cts = new CancellationTokenSource(5); await cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -66,7 +93,7 @@ await TestCancellation(async (cws) => var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -87,7 +114,7 @@ await TestCancellation(async (cws) => var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(".delay5sec"), + WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -146,12 +173,16 @@ protected async Task RunClient_ReceiveAsync_AfterCancellationDoReceiveAsync_Thro ex.Message); } } + + #endregion } [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] - public class CancelTest_External(ITestOutputHelper output) : CancelTestBase(output) + 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) @@ -184,8 +215,11 @@ public Task ReceiveAsync_ReceiveThenCancel_ThrowsOperationCanceledException(Uri [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) @@ -197,4 +231,5 @@ public sealed class CancelTest_HttpClient_External(ITestOutputHelper output) : C { 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 41ac96104237c4..cd37693a8fc994 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs @@ -80,8 +80,7 @@ public async Task Proxy_ConnectThruProxy_Success(Uri server) server, TimeOutMilliseconds, _output, - default(TimeSpan), - proxy)) + proxy: proxy)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); Assert.Equal(WebSocketState.Open, cws.State); @@ -116,7 +115,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.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Loopback.cs new file mode 100644 index 00000000000000..0558d58b267113 --- /dev/null +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Loopback.cs @@ -0,0 +1,56 @@ +// 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 = HttpVersion, + UseSsl = useSsl, + SkipServerHandshakeResponse = true, + IgnoreServerErrors = true, + AbortServerOnClientExit = true, + ParseEchoOptions = true, + Output = _output + }; + + 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 = HttpVersion, + UseSsl = useSsl, + IgnoreServerErrors = true, + AbortServerOnClientExit = true, + Output = _output + }; + + 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 17964d85aa283e..28a8ab88b20bb1 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs @@ -7,27 +7,23 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Microsoft.DotNet.XUnitExtensions; -using System.Net.Test.Common; 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[][] - { - [ o[0], false ], - [ 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 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) @@ -41,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 { @@ -81,22 +72,16 @@ public async Task TestCancellation(Func action, Uri serve try { await action(cws); - // Operation finished before CTS expired. - } - catch (OperationCanceledException exception) - { - // 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}"); + _output.WriteLine($"Operation finished before CTS expired."); } - catch (WebSocketException exception) + catch (Exception e) when (e is OperationCanceledException or ObjectDisposedException or WebSocketException) { - 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}"); + } } } } @@ -153,17 +138,11 @@ protected static async Task ReceiveEntireMessageAsync(We return new HttpClient(handler); } - protected Task GetConnectedWebSocket(Uri uri, int timeOutMilliseconds, ITestOutputHelper output) - => WebSocketHelper.GetConnectedWebSocket(uri, timeOutMilliseconds, output, o => ConfigureHttpVersion(o, uri), GetInvoker()); - protected Task GetConnectedWebSocket(Uri uri) - => GetConnectedWebSocket(uri, TimeOutMilliseconds, _output); + => WebSocketHelper.GetConnectedWebSocket(uri, TimeOutMilliseconds, _output, o => ConfigureHttpVersion(o, uri), GetInvoker()); protected Task ConnectAsync(ClientWebSocket cws, Uri uri, CancellationToken cancellationToken) - { - ConfigureHttpVersion(cws.Options, uri); - return cws.ConnectAsync(uri, GetInvoker(), cancellationToken); - } + => WebSocketHelper.ConnectAsync(cws, uri, o => ConfigureHttpVersion(o, uri), GetInvoker(), validateState: false, cancellationToken); protected Task TestEcho(Uri uri, WebSocketMessageType type) => WebSocketHelper.TestEcho(uri, type, TimeOutMilliseconds, _output, o => ConfigureHttpVersion(o, uri), GetInvoker()); @@ -172,21 +151,12 @@ protected void ConfigureHttpVersion(ClientWebSocketOptions options, Uri uri) { if (PlatformDetection.IsBrowser) { - if (HttpVersion != Net.HttpVersion.Version11) - { - throw new SkipTestException($"HTTP version {HttpVersion} is not supported for WebSockets on Browser."); - } return; } options.HttpVersion = HttpVersion; options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; - if (UseSharedHandler && uri.Scheme == "wss") - { - options.RemoteCertificateValidationCallback = (_, _, _, _) => true; - } - if (HttpVersion == Net.HttpVersion.Version20 && uri.Query is not (null or "" or "?")) { // RFC 7540, section 8.3. The CONNECT Method: @@ -200,64 +170,5 @@ protected void ConfigureHttpVersion(ClientWebSocketOptions options, Uri uri) } public static bool WebSocketsSupported { get { return WebSocketHelper.WebSocketsSupported; } } - -#if !TARGET_BROWSER // Loopback server related functions - - protected virtual bool SkipIfUseSsl => false; - - protected Task RunEchoAsync(Func clientFunc, bool useSsl) - { - if (SkipIfUseSsl && useSsl) - { - throw new SkipTestException("SSL is not supported in this test."); - } - - var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); - var options = new LoopbackWebSocketServer.Options - { - HttpVersion = HttpVersion, - UseSsl = useSsl, - SkipServerHandshakeResponse = true, - IgnoreServerErrors = true, - AbortServerOnClientExit = true, - ParseEchoOptions = true, - Output = _output - }; - - return LoopbackWebSocketServer.RunEchoAsync(clientFunc, options, timeoutCts.Token); - } - - protected Task RunEchoHeadersAsync(Func clientFunc, bool useSsl) - { - if (SkipIfUseSsl && useSsl) - { - throw new SkipTestException("SSL is not supported in this test."); - } - - var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); - var options = new LoopbackWebSocketServer.Options - { - HttpVersion = HttpVersion, - UseSsl = useSsl, - IgnoreServerErrors = true, - AbortServerOnClientExit = true, - Output = _output - }; - - 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); - } -#endif } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs index 3456c2913c9a92..f11d7982724268 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs @@ -11,45 +11,46 @@ namespace System.Net.WebSockets.Client.Tests { - // --- Loopback Echo Server "overrides" --- [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets are not supported on browser")] - public abstract class CloseTest_Loopback(ITestOutputHelper output) : CloseTestBase(output) + 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_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task CloseAsync_ClientInitiatedClose_Success(bool useSsl) => RunEchoAsync( RunClient_CloseAsync_ClientInitiatedClose_Success, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task CloseAsync_CloseDescriptionIsMaxLength_Success(bool useSsl) => RunEchoAsync( RunClient_CloseAsync_CloseDescriptionIsMaxLength_Success, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException(bool useSsl) => RunEchoAsync( RunClient_CloseAsync_CloseDescriptionIsMaxLengthPlusOne_ThrowsArgumentException, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task CloseAsync_CloseDescriptionHasUnicode_Success(bool useSsl) => RunEchoAsync( RunClient_CloseAsync_CloseDescriptionHasUnicode_Success, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task CloseAsync_CloseDescriptionIsNull_Success(bool useSsl) => RunEchoAsync( RunClient_CloseAsync_CloseDescriptionIsNull_Success, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task CloseOutputAsync_ExpectedStates(bool useSsl) => RunEchoAsync( RunClient_CloseOutputAsync_ExpectedStates, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task CloseAsync_CloseOutputAsync_Throws(bool useSsl) => RunEchoAsync( RunClient_CloseAsync_CloseOutputAsync_Throws, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(bool useSsl) => RunEchoAsync( RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanClose, useSsl); @@ -57,7 +58,7 @@ public Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(bool useSsl) => public Task CloseOutputAsync_ServerInitiated_CanReceive(bool useSsl, bool delayReceiving) => RunEchoAsync( server => RunClient_CloseOutputAsync_ServerInitiated_CanReceive(server, delayReceiving), useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task CloseOutputAsync_ServerInitiated_CanSend(bool useSsl) => RunEchoAsync( RunClient_CloseOutputAsync_ServerInitiated_CanSend, useSsl); @@ -65,24 +66,26 @@ public Task CloseOutputAsync_ServerInitiated_CanSend(bool useSsl) => RunEchoAsyn public Task CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(bool useSsl, bool syncState) => RunEchoAsync( server => RunClient_CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(server, syncState), useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [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_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates(bool useSsl) => RunEchoAsync( RunClient_CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates(bool useSsl) => RunEchoAsync( RunClient_CloseAsync_DuringConcurrentReceiveAsync_ExpectedStates, useSsl); - } - // --- HTTP/1.1 WebSocket loopback tests --- + #endregion + } - public sealed class CloseTest_SharedHandler_Loopback(ITestOutputHelper output) : CloseTest_Loopback(output) + public abstract class CloseTest_Loopback(ITestOutputHelper output) : CloseTest_LoopbackBase(output) { + #region HTTP/1.1-only loopback tests + [Fact] public async Task CloseAsync_CancelableEvenWhenPendingReceive_Throws() { @@ -136,7 +139,7 @@ public async Task CloseHandshake_ExceptionsAreObserved() { await RemoteExecutor.Invoke(static (typeName) => { - CloseTest_External test = (CloseTest_External)Activator.CreateInstance(typeof(CloseTest_External).Assembly.GetType(typeName), new object[] { null }); + ClientWebSocketTestBase test = (ClientWebSocketTestBase)Activator.CreateInstance(typeof(ClientWebSocketTestBase).Assembly.GetType(typeName), new object[] { null }); using CancellationTokenSource timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); Exception unobserved = null; @@ -165,8 +168,19 @@ await RemoteExecutor.Invoke(static (typeName) => }, new LoopbackWebSocketServer.Options { HttpVersion = Net.HttpVersion.Version11, UseSsl = true, HttpInvoker = test.GetInvoker() }, 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; @@ -177,12 +191,9 @@ public sealed class CloseTest_HttpClient_Loopback(ITestOutputHelper output) : Cl protected override bool UseHttpClient => true; } - // --- HTTP/2 WebSocket loopback tests --- + #endregion - public abstract class CloseTest_Http2Loopback(ITestOutputHelper output) : CloseTest_Loopback(output) - { - internal override Version HttpVersion => Net.HttpVersion.Version20; - } + #region Runnable test classes: HTTP/2 Loopback public sealed class CloseTest_Invoker_Http2Loopback(ITestOutputHelper output) : CloseTest_Http2Loopback(output) { @@ -193,4 +204,5 @@ public sealed class CloseTest_HttpClient_Http2Loopback(ITestOutputHelper 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 f0cc28d86ce486..efa31fbe8cbf23 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs @@ -8,21 +8,42 @@ using Xunit; using Xunit.Abstractions; +using EchoControlMessage = System.Net.Test.Common.WebSocketEchoHelper.EchoControlMessage; + namespace System.Net.WebSockets.Client.Tests { + // + // 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) { - // [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] - protected async Task RunClient_CloseAsync_ServerInitiatedClose_Success(Uri server, bool useCloseOutputAsync) // -- TO MOVE to loopback and close issue - { - const string shutdownWebSocketMetaCommand = ".shutdown"; + #region Common (Echo Server) tests + protected async Task RunClient_CloseAsync_ServerInitiatedClose_Success(Uri server, bool useCloseOutputAsync) + { using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(shutdownWebSocketMetaCommand), + WebSocketData.GetBufferFromText(EchoControlMessage.Shutdown), WebSocketMessageType.Text, true, cts.Token); @@ -32,13 +53,13 @@ await cws.SendAsync( // 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. var closeStatus = PlatformDetection.IsNotBrowser ? WebSocketCloseStatus.InvalidMessageType : (WebSocketCloseStatus)3210; @@ -50,7 +71,7 @@ await cws.SendAsync( // 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); } } @@ -216,11 +237,10 @@ protected async Task RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanCl } } - // [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] -- TO MOVE to loopback and close issue 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)) { @@ -271,19 +291,18 @@ await cws.SendAsync( } } - // [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] -- TO MOVE to loopback and close issue 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)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(".shutdown"), + WebSocketData.GetBufferFromText(EchoControlMessage.Shutdown), WebSocketMessageType.Text, true, cts.Token); @@ -317,14 +336,13 @@ await cws.SendAsync( } } - // [ActiveIssue("https://github.com/dotnet/runtime/issues/28957", typeof(PlatformDetection), nameof(PlatformDetection.IsNotBrowser))] -- TO MOVE to loopback and close issue protected async Task RunClient_CloseOutputAsync_ServerInitiated_CanReceiveAfterClose(Uri server, bool syncState) { using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(".receiveMessageAfterClose"), + WebSocketData.GetBufferFromText(EchoControlMessage.ReceiveMessageAfterClose), WebSocketMessageType.Text, true, cts.Token); @@ -342,7 +360,7 @@ await cws.SendAsync( WebSocketReceiveResult recvResult = await cws.ReceiveAsync(recvBuffer, cts.Token); var message = Encoding.UTF8.GetString(recvBuffer.ToArray(), 0, recvResult.Count); - Assert.Contains(".receiveMessageAfterClose", message); + Assert.Contains(EchoControlMessage.ReceiveMessageAfterClose, message); } } @@ -359,7 +377,6 @@ protected async Task RunClient_CloseOutputAsync_CloseDescriptionIsNull_Success(U } } - // [ActiveIssue("https://github.com/dotnet/runtime/issues/22000", TargetFrameworkMonikers.Netcoreapp)] protected async Task RunClient_CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates(Uri server) { var receiveBuffer = new byte[1024]; @@ -419,13 +436,17 @@ protected async Task RunClient_CloseAsync_DuringConcurrentReceiveAsync_ExpectedS } } } + + #endregion } [OuterLoop("Uses external servers", typeof(PlatformDetection), nameof(PlatformDetection.LocalEchoServerIsNotAvailable))] [ConditionalClass(typeof(ClientWebSocketTestBase), nameof(WebSocketsSupported))] public abstract class CloseTest_External(ITestOutputHelper output) : CloseTestBase(output) { - [ConditionalTheory(nameof(PlatformDetection), nameof(PlatformDetection.IsBrowser))] // See https://github.com/dotnet/runtime/issues/28957 + #region Common (Echo Server) tests + + [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); @@ -462,17 +483,17 @@ public Task CloseAsync_CloseOutputAsync_Throws(Uri server) public Task CloseOutputAsync_ClientInitiated_CanReceive_CanClose(Uri server) => RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanClose(server); - [ConditionalTheory(nameof(PlatformDetection), nameof(PlatformDetection.IsBrowser))] // See https://github.com/dotnet/runtime/issues/28957 + [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); - [ConditionalTheory(nameof(PlatformDetection), nameof(PlatformDetection.IsBrowser))] // See https://github.com/dotnet/runtime/issues/28957 + [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(nameof(PlatformDetection), nameof(PlatformDetection.IsBrowser))] // See https://github.com/dotnet/runtime/issues/28957 + [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); @@ -489,8 +510,12 @@ public Task CloseOutputAsync_DuringConcurrentReceiveAsync_ExpectedStates(Uri ser [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) @@ -502,4 +527,6 @@ public sealed class CloseTest_HttpClient_External(ITestOutputHelper output) : Cl { 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 ec857ea54fa91e..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,25 +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 { - // --- HTTP/2 WebSocket loopback tests --- - - public abstract class ConnectTest_Http2Loopback(ITestOutputHelper output) : ConnectTest_Loopback(output) + public abstract partial class ConnectTest_Http2Loopback { - internal override Version HttpVersion => Net.HttpVersion.Version20; + #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 => @@ -45,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 => @@ -71,7 +65,6 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => } [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 => @@ -93,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 => @@ -115,7 +107,6 @@ 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 => @@ -146,123 +137,6 @@ await Http2LoopbackServer.CreateClientAndServerAsync(async uri => }, new Http2Options() { WebSocketEndpoint = true, UseSsl = false }); } - [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 = 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); - } - } - - [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 = Net.HttpVersion.Version20; - cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; - await cws.ConnectAsync(server, GetInvoker(), cts.Token); - Assert.Equal(WebSocketState.Open, 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_Http11WithRequestVersionOrHigher_DowngradeSuccess(Uri server) - { - using (var cws = new ClientWebSocket()) - using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) - { - cws.Options.HttpVersion = Net.HttpVersion.Version11; - cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher; - 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_Http11WithRequestVersionOrHigher_Loopback_DowngradeSuccess() - { - 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 = true, WebSocketEndpoint = true }); - } - } - - 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; - } - - public sealed class ConnectTest_SharedHandler_Http2(ITestOutputHelper output) : ClientWebSocketTestBase(output) - { - 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.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 index 1ed9053bd894cf..be20b1aa16ebe7 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.Loopback.cs @@ -1,12 +1,8 @@ // 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.Runtime.InteropServices; -using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Microsoft.DotNet.XUnitExtensions; @@ -17,121 +13,117 @@ 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_Loopback(ITestOutputHelper output) : ConnectTestBase(output) + public abstract class ConnectTest_LoopbackBase(ITestOutputHelper output) : ConnectTestBase(output) { - // --- Loopback Echo Server "overrides" --- + #region Common (Echo Server) tests - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task EchoBinaryMessage_Success(bool useSsl) => RunEchoAsync( RunClient_EchoBinaryMessage_Success, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task EchoTextMessage_Success(bool useSsl) => RunEchoAsync( RunClient_EchoTextMessage_Success, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task ConnectAsync_AddCustomHeaders_Success(bool useSsl) => RunEchoHeadersAsync( RunClient_ConnectAsync_AddCustomHeaders_Success, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task ConnectAsync_CookieHeaders_Success(bool useSsl) => RunEchoHeadersAsync( RunClient_ConnectAsync_CookieHeaders_Success, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException(bool useSsl) => RunEchoAsync( RunClient_ConnectAsync_PassNoSubProtocol_ServerRequires_ThrowsWebSocketException, useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] + [Theory, MemberData(nameof(UseSsl))] public Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol(bool useSsl) => RunEchoAsync( RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesAgreedSubProtocol, useSsl); - } - - // --- HTTP/1.1 WebSocket loopback tests --- - public abstract class ConnectTest_Loopback_Http11(ITestOutputHelper output) : ConnectTest_Loopback(output) - { - [Theory, MemberData(nameof(UseSsl_MemberData))] + [ConditionalTheory] // Uses SkipTestException + [MemberData(nameof(UseSsl))] public Task ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState(bool useSsl) => RunEchoAsync( RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClosedState, useSsl); + + #endregion } - public sealed class ConnectTest_Invoker_Loopback(ITestOutputHelper output) : ConnectTest_Loopback_Http11(output) + public abstract class ConnectTest_Loopback(ITestOutputHelper output) : ConnectTest_LoopbackBase(output) { - protected override bool UseCustomInvoker => true; + #region HTTP/1.1-only loopback tests - public static IEnumerable ConnectAsync_CustomInvokerWithIncompatibleWebSocketOptions_ThrowsArgumentException_MemberData() + [ConditionalFact] // Uses SkipTestException + public async Task ConnectAsync_Http11WithRequestVersionOrHigher_Loopback_DowngradeSuccess() { - 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) + if (UseSharedHandler) { - yield return Throw(options => options.ClientCertificates.Add(Test.Common.Configuration.Certificates.GetClientCertificate())); + throw new SkipTestException("HTTP/2 is not supported with SharedHandler"); } - 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()); + 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 = true, WebSocketEndpoint = true }); + } - // We allow no proxy or the default proxy to be used - yield return NoThrow(options => { }); - yield return NoThrow(options => options.Proxy = null); + #endregion + } - // 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); + public abstract partial class ConnectTest_Http2Loopback(ITestOutputHelper output) : ConnectTest_LoopbackBase(output) + { + internal override Version HttpVersion => Net.HttpVersion.Version20; - static object[] Throw(Action configureOptions) => - new object[] { configureOptions, true }; + // #region HTTP/2-only loopback tests -> extracted to ConnectTest.Http2.cs + } - static object[] NoThrow(Action configureOptions) => - new object[] { configureOptions, false }; - } + #region Runnable test classes: HTTP/1.1 Loopback - [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")) - }); + 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 + } - using var ws = new ClientWebSocket(); - configureOptions(ws.Options); + public sealed partial class ConnectTest_Invoker_Loopback(ITestOutputHelper output) : ConnectTest_Loopback(output) + { + protected override bool UseCustomInvoker => true; - 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); - } + // #region Invoker-only HTTP/1.1 loopback tests -> extracted to ConnectTest.Invoker.cs + } - foreach (X509Certificate cert in ws.Options.ClientCertificates) - { - cert.Dispose(); - } - } + 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_Loopback(ITestOutputHelper output) : ConnectTest_Loopback_Http11(output) + 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 index 256f1f2f7c3d2a..193d0706dd9629 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.SharedHandler.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.SharedHandler.cs @@ -5,18 +5,16 @@ using System.IO; using System.Net.Http; using System.Net.Test.Common; -using System.Runtime.InteropServices; -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 sealed class ConnectTest_SharedHandler_Loopback(ITestOutputHelper output) : ConnectTest_Loopback_Http11(output) + public partial class ConnectTest_SharedHandler_Loopback { + #region SharedHandler-only HTTP/1.1 loopback tests + [Fact] public async Task ConnectAsync_CancellationRequestedBeforeConnect_ThrowsOperationCanceledException() { @@ -173,5 +171,35 @@ await LoopbackServer.CreateClientAndServerAsync(async uri => 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 3f28c9797edead..384e4804d7e1d5 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs @@ -1,6 +1,7 @@ // 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; @@ -8,10 +9,35 @@ using Xunit; using Xunit.Abstractions; +using EchoQueryKey = System.Net.Test.Common.WebSocketEchoOptions.EchoQueryKey; + namespace System.Net.WebSockets.Client.Tests { + // + // 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) { + #region Common (Echo Server) tests + protected async Task RunClient_EchoBinaryMessage_Success(Uri server) { await TestEcho(server, WebSocketMessageType.Binary); @@ -121,8 +147,7 @@ protected async Task RunClient_ConnectAsync_PassNoSubProtocol_ServerRequires_Thr { 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)); @@ -145,8 +170,7 @@ protected async Task RunClient_ConnectAsync_PassMultipleSubProtocols_ServerRequi 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); @@ -185,12 +209,16 @@ protected async Task RunClient_ConnectAndCloseAsync_UseProxyServer_ExpectedClose Assert.Equal(1, proxyServer.Connections); } } + + #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); @@ -223,8 +251,13 @@ public Task ConnectAsync_PassMultipleSubProtocols_ServerRequires_ConnectionUsesA 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))] + [Theory] + [MemberData(nameof(UnavailableWebSocketServers))] public async Task ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMessage(Uri server, string exceptionMessage, WebSocketError errorCode) { using (var cws = new ClientWebSocket()) @@ -247,12 +280,82 @@ public async Task ConnectAsync_NotWebSocketServer_ThrowsWebSocketExceptionWithMe await Assert.ThrowsAsync(() => cws.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, null, default)); } } - } - public sealed class ConnectTest_SharedHandler_External(ITestOutputHelper output) : ConnectTest_External(output) - { + [SkipOnPlatform(TestPlatforms.Browser, "HTTP/2 WebSockets are not supported on this platform")] + [ConditionalFact] // Uses SkipTestException + public async Task ConnectAsync_Http11Server_DowngradeFail() + { + if (UseSharedHandler) + { + throw new SkipTestException("HTTP/2 is not supported with SharedHandler"); + } + + using (var cws = new ClientWebSocket()) + using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) + { + 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); + } + } + + [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) + { + if (UseSharedHandler) + { + throw new SkipTestException("HTTP/2 is not supported with SharedHandler"); + } + + using (var cws = new ClientWebSocket()) + using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) + { + cws.Options.HttpVersion = Net.HttpVersion.Version20; + cws.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionOrLower; + await cws.ConnectAsync(server, GetInvoker(), cts.Token); + Assert.Equal(WebSocketState.Open, cws.State); + } + } + + [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) + { + if (UseSharedHandler) + { + throw new SkipTestException("HTTP/2 is not supported with SharedHandler"); + } + + using (var cws = new ClientWebSocket()) + using (var cts = new CancellationTokenSource(TimeOutMilliseconds)) + { + 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; @@ -262,4 +365,6 @@ public sealed class ConnectTest_HttpClient_External(ITestOutputHelper 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 7811463565a5f6..0c5e4977a4d5df 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/DeflateTests.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/DeflateTests.cs @@ -14,18 +14,20 @@ namespace System.Net.WebSockets.Client.Tests { - public sealed class InvokerDeflateTests(ITestOutputHelper output) : DeflateTests(output) - { - protected override bool UseCustomInvoker => true; - } - - public sealed class HttpClientDeflateTests(ITestOutputHelper output) : DeflateTests(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(ITestOutputHelper output) : ClientWebSocketTestBase(output) + public abstract class DeflateTests(ITestOutputHelper output) : ClientWebSocketTestBase(output) { [ConditionalTheory(nameof(WebSocketsSupported))] [InlineData(15, true, 15, true, "permessage-deflate; client_max_window_bits")] @@ -191,4 +193,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 527b0a91df2627..d1042e6c698c89 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs @@ -9,12 +9,29 @@ 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(ITestOutputHelper output) : ClientWebSocketTestBase(output) { + #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 }; @@ -104,36 +121,42 @@ private 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 KeepAliveTest_Http2Loopback(ITestOutputHelper output) : KeepAliveTest_Loopback(output) + { + internal override Version HttpVersion => Net.HttpVersion.Version20; + } - public class KeepAliveTest_SharedHandler_Loopback(ITestOutputHelper output) : KeepAliveTest_Loopback(output) { } + #region Runnable test classes: HTTP/1.1 Loopback - public class KeepAliveTest_Invoker_Loopback(ITestOutputHelper output) : KeepAliveTest_Loopback(output) + public sealed class KeepAliveTest_SharedHandler_Loopback(ITestOutputHelper output) : KeepAliveTest_Loopback(output) { } + + public sealed class KeepAliveTest_Invoker_Loopback(ITestOutputHelper output) : KeepAliveTest_Loopback(output) { protected override bool UseCustomInvoker => true; } - public class KeepAliveTest_HttpClient_Loopback(ITestOutputHelper output) : KeepAliveTest_Loopback(output) + public sealed class KeepAliveTest_HttpClient_Loopback(ITestOutputHelper output) : KeepAliveTest_Loopback(output) { protected override bool UseHttpClient => true; } - // --- HTTP/2 WebSocket loopback tests --- + #endregion - public class KeepAliveTest_Http2Loopback(ITestOutputHelper output) : KeepAliveTest_Loopback(output) - { - internal override Version HttpVersion => Net.HttpVersion.Version20; - } + #region Runnable test classes: HTTP/2 Loopback - public class KeepAliveTest_Invoker_Http2Loopback(ITestOutputHelper output) : KeepAliveTest_Http2Loopback(output) + public sealed class KeepAliveTest_Invoker_Http2Loopback(ITestOutputHelper output) : KeepAliveTest_Http2Loopback(output) { protected override bool UseCustomInvoker => true; } - public class KeepAliveTest_HttpClient_Http2Loopback(ITestOutputHelper output) : KeepAliveTest_Http2Loopback(output) + public sealed class KeepAliveTest_HttpClient_Http2Loopback(ITestOutputHelper output) : KeepAliveTest_Http2Loopback(output) { protected override bool UseHttpClient => true; } + + #endregion } 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 44abfe13ba927a..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,24 +8,14 @@ using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace System.Net.WebSockets.Client.Tests { - public sealed class HttpClientSendReceiveTest_Http2(ITestOutputHelper output) : SendReceiveTest_Http2(output) + public abstract partial class SendReceiveTest_Http2Loopback { - protected override bool UseHttpClient => true; - } - - public sealed class InvokerSendReceiveTest_Http2(ITestOutputHelper output) : SendReceiveTest_Http2(output) - { - protected override bool UseCustomInvoker => true; - } + #region HTTP/2-only loopback tests - public abstract class SendReceiveTest_Http2(ITestOutputHelper output) : ClientWebSocketTestBase(output) - { [Fact] - [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets is not supported on this platform")] public async Task ReceiveNoThrowAfterSend_NoSsl() { var serverMessage = new byte[] { 4, 5, 6 }; @@ -63,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 }; @@ -99,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 index 999e18f2f7ebd6..bd09d929bb02c2 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs @@ -11,137 +11,164 @@ 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_Loopback(ITestOutputHelper output) : SendReceiveTestBase(output) + public abstract class SendReceiveTest_LoopbackBase(ITestOutputHelper output) : SendReceiveTestBase(output) { - [Theory, MemberData(nameof(UseSsl_MemberData))] - public Task SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(Uri server) - => RunClient_SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(server); + #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); - [Theory, MemberData(nameof(UseSsl_MemberData))] - public Task SendReceive_PartialMessageBeforeCompleteMessageArrives_Success(Uri server) - => RunClient_SendReceive_PartialMessageBeforeCompleteMessageArrives_Success(server); + [Theory, MemberData(nameof(UseSslAndSendReceiveType))] + public Task SendReceive_PartialMessageBeforeCompleteMessageArrives_Success(bool useSsl, SendReceiveType type) => RunEchoAsync( + server => RunSendReceive(RunClient_SendReceive_PartialMessageBeforeCompleteMessageArrives_Success, server, type), useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] - public Task SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage(Uri server) - => RunClient_SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage(server); + [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(UseSsl_MemberData))] - public Task SendAsync_MultipleOutstandingSendOperations_Throws(Uri server) - => RunClient_SendAsync_MultipleOutstandingSendOperations_Throws(server); + [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(UseSsl_MemberData))] - public Task ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(Uri server) - => RunClient_ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(server); + [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(UseSsl_MemberData))] - public Task SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(Uri server) - => RunClient_SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(server); + [Theory, MemberData(nameof(UseSslAndSendReceiveType))] + public Task SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(bool useSsl, SendReceiveType type) => RunEchoAsync( + server => RunSendReceive(RunClient_SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success, server, type), useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] - public Task SendReceive_VaryingLengthBuffers_Success(Uri server) - => RunClient_SendReceive_VaryingLengthBuffers_Success(server); + [Theory, MemberData(nameof(UseSslAndSendReceiveType))] + public Task SendReceive_VaryingLengthBuffers_Success(bool useSsl, SendReceiveType type) => RunEchoAsync( + server => RunSendReceive(RunClient_SendReceive_VaryingLengthBuffers_Success, server, type), useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] - public Task SendReceive_Concurrent_Success(Uri server) - => RunClient_SendReceive_Concurrent_Success(server); + [Theory, MemberData(nameof(UseSslAndSendReceiveType))] + public Task SendReceive_Concurrent_Success(bool useSsl, SendReceiveType type) => RunEchoAsync( + server => RunSendReceive(RunClient_SendReceive_Concurrent_Success, server, type), useSsl); - [Fact] - public Task SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated() - => RunClient_SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated(); + [Theory, MemberData(nameof(UseSslAndSendReceiveType))] + public Task ZeroByteReceive_CompletesWhenDataAvailable(bool useSsl, SendReceiveType type) => RunEchoAsync( + server => RunSendReceive(RunClient_ZeroByteReceive_CompletesWhenDataAvailable, server, type), useSsl); - [Theory, MemberData(nameof(UseSsl_MemberData))] - public Task ZeroByteReceive_CompletesWhenDataAvailable(Uri server) - => RunClient_ZeroByteReceive_CompletesWhenDataAvailable(server); + #endregion } -#region Memory send/receive tests - public abstract class MemorySendReceiveTest_Loopback(ITestOutputHelper output) : SendReceiveTest_Loopback(output) + public abstract class SendReceiveTest_Loopback(ITestOutputHelper output) : SendReceiveTest_LoopbackBase(output) { - protected override async Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) + #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() { - ValueWebSocketReceiveResult r = await ws.ReceiveAsync( - (Memory)arraySegment, - cancellationToken).ConfigureAwait(false); - return new WebSocketReceiveResult(r.Count, r.MessageType, r.EndOfMessage, ws.CloseStatus, ws.CloseStatusDescription); - } + var options = new LoopbackServer.Options { WebSocketEndpoint = true }; - protected override Task SendAsync(WebSocket ws, ArraySegment arraySegment, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) => - ws.SendAsync( - (ReadOnlyMemory)arraySegment, - messageType, - endOfMessage, - cancellationToken).AsTask(); - } + Func connectToServerThatAbortsConnection = async (clientSocket, server, url) => + { + var pendingReceiveAsyncPosted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - public sealed class MemorySendReceiveTest_SharedHandler_Loopback(ITestOutputHelper output) : MemorySendReceiveTest_Loopback(output) { } + // 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)); - public sealed class MemorySendReceiveTest_Invoker_Loopback(ITestOutputHelper output) : MemorySendReceiveTest_Loopback(output) - { - protected override bool UseCustomInvoker => true; - } + // Wait for client-side ConnectAsync to complete and for a pending ReceiveAsync to be posted. + await pendingReceiveAsyncPosted.Task.WaitAsync(TimeSpan.FromMilliseconds(TimeOutMilliseconds)); - public sealed class MemorySendReceiveTest_HttpClient_Loopback(ITestOutputHelper output) : MemorySendReceiveTest_Loopback(output) - { - protected override bool UseHttpClient => true; - } + // Close the underlying connection prematurely (without sending a WebSocket Close frame). + connection.Socket.Shutdown(SocketShutdown.Both); + connection.Socket.Close(); + }); - // --- HTTP/2 WebSocket loopback tests --- + // Initiate a connection attempt. + var cts = new CancellationTokenSource(TimeOutMilliseconds); + await ConnectAsync(clientSocket, url, cts.Token); - public abstract class MemorySendReceiveTest_Http2Loopback(ITestOutputHelper output) : MemorySendReceiveTest_Loopback(output) - { - internal override Version HttpVersion => Net.HttpVersion.Version20; - } + // 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(); - public sealed class MemorySendReceiveTest_Invoker_Http2Loopback(ITestOutputHelper output) : MemorySendReceiveTest_Http2Loopback(output) - { - protected override bool UseCustomInvoker => true; - } + // Wait for the server to close the underlying connection. + await acceptTask.WaitAsync(cts.Token); - public sealed class MemorySendReceiveTest_HttpClient_Http2Loopback(ITestOutputHelper output) : MemorySendReceiveTest_Http2Loopback(output) - { - protected override bool UseHttpClient => true; + 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 } -#endregion -#region ArraySegment send/receive tests - public abstract class ArraySegmentSendReceiveTest(ITestOutputHelper output) : SendReceiveTest_Loopback(output) + public abstract partial class SendReceiveTest_Http2Loopback(ITestOutputHelper output) : SendReceiveTest_LoopbackBase(output) { - protected override Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) => - ws.ReceiveAsync(arraySegment, cancellationToken); + internal override Version HttpVersion => Net.HttpVersion.Version20; - protected override Task SendAsync(WebSocket ws, ArraySegment arraySegment, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) => - ws.SendAsync(arraySegment, messageType, endOfMessage, cancellationToken); + // #region HTTP/2-only loopback tests -> extracted to SendReceiveTest.Http2.cs } - public sealed class ArraySegmentSendReceiveTest_SharedHandler_Loopback(ITestOutputHelper output) : ArraySegmentSendReceiveTest_Loopback(output) { } + /*#region Runnable test classes: HTTP/1.1 Loopback + + public sealed class SendReceiveTest_SharedHandler_Loopback(ITestOutputHelper output) : SendReceiveTest_Loopback(output) { } - public sealed class ArraySegmentSendReceiveTest_Invoker_Loopback(ITestOutputHelper output) : ArraySegmentSendReceiveTest_Loopback(output) + public sealed class SendReceiveTest_Invoker_Loopback(ITestOutputHelper output) : SendReceiveTest_Loopback(output) { protected override bool UseCustomInvoker => true; } - public sealed class ArraySegmentSendReceiveTest_HttpClient_Loopback(ITestOutputHelper output) : ArraySegmentSendReceiveTest_Loopback(output) + public sealed class SendReceiveTest_HttpClient_Loopback(ITestOutputHelper output) : SendReceiveTest_Loopback(output) { protected override bool UseHttpClient => true; } - // --- HTTP/2 WebSocket loopback tests --- + #endregion*/ - public abstract class ArraySegmentSendReceiveTest_Http2Loopback(ITestOutputHelper output) : ArraySegmentSendReceiveTest_Loopback(output) - { - internal override Version HttpVersion => Net.HttpVersion.Version20; - } + /*#region Runnable test classes: HTTP/2 Loopback - public sealed class ArraySegmentSendReceiveTest_Invoker_Http2Loopback(ITestOutputHelper output) : ArraySegmentSendReceiveTest_Http2Loopback(output) + public sealed class SendReceiveTest_Invoker_Http2Loopback(ITestOutputHelper output) : SendReceiveTest_Http2Loopback(output) { protected override bool UseCustomInvoker => true; } - public sealed class ArraySegmentSendReceiveTest_HttpClient_Http2Loopback(ITestOutputHelper output) : ArraySegmentSendReceiveTest_Http2Loopback(output) + public sealed class SendReceiveTest_HttpClient_Http2Loopback(ITestOutputHelper output) : SendReceiveTest_Http2Loopback(output) { protected override bool UseHttpClient => true; } -#endregion + + #endregion*/ } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs index cc04e984281d19..df576cb84cb980 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.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.Net.Sockets; -using System.Net.Test.Common; using System.Threading; using System.Threading.Tasks; @@ -11,59 +9,79 @@ namespace System.Net.WebSockets.Client.Tests { + // + // 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 sealed class InvokerMemorySendReceiveTest(ITestOutputHelper output) : MemorySendReceiveTest(output) + public abstract class SendReceiveTestBase(ITestOutputHelper output) : ClientWebSocketTestBase(output) { - protected override bool UseCustomInvoker => true; - } + #region Send-receive type test setup - public sealed class HttpClientMemorySendReceiveTest(ITestOutputHelper output) : MemorySendReceiveTest(output) - { - protected override bool UseHttpClient => true; - } + public enum SendReceiveType + { + ArraySegment, + Memory + } - public sealed class InvokerArraySegmentSendReceiveTest(ITestOutputHelper output) : ArraySegmentSendReceiveTest(output) - { - protected override bool UseCustomInvoker => true; - } + public static readonly object[][] EchoServersAndSendReceiveType = ToMemberData(EchoServers_Values, Enum.GetValues()); - public sealed class HttpClientArraySegmentSendReceiveTest(ITestOutputHelper output) : ArraySegmentSendReceiveTest(output) - { - protected override bool UseHttpClient => true; - } + public static readonly object[][] UseSslAndSendReceiveType = ToMemberData(UseSsl_Values, Enum.GetValues()); + public static readonly object[][] SendReceiveTypes = ToMemberData(Enum.GetValues()); - public class MemorySendReceiveTest(ITestOutputHelper output) : SendReceiveTest(output) - { + protected SendReceiveType? SendReceiveTestType { get; set; } + + protected Task SendAsync(WebSocket ws, ArraySegment arraySegment, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + => SendReceiveTestType switch + { + SendReceiveType.ArraySegment => ws.SendAsync(arraySegment, messageType, endOfMessage, cancellationToken), + SendReceiveType.Memory => ws.SendAsync((ReadOnlyMemory)arraySegment, messageType, endOfMessage, cancellationToken).AsTask(), + _ => throw new ArgumentException(nameof(SendReceiveTestType)) + }; + + protected Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) + => SendReceiveTestType switch + { + SendReceiveType.ArraySegment => ws.ReceiveAsync(arraySegment, cancellationToken), + SendReceiveType.Memory => Task.Run(async () => + { + ValueWebSocketReceiveResult r = await ws.ReceiveAsync((Memory)arraySegment, cancellationToken).ConfigureAwait(false); + return new WebSocketReceiveResult(r.Count, r.MessageType, r.EndOfMessage, ws.CloseStatus, ws.CloseStatusDescription); + }), + _ => throw new ArgumentException(nameof(SendReceiveTestType)) + }; - 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); + SendReceiveTestType = 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(); - } - public class ArraySegmentSendReceiveTest(ITestOutputHelper output) : SendReceiveTest(output) - { - protected override Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) => - ws.ReceiveAsync(arraySegment, cancellationToken); + protected Task RunSendReceive(Func sendReceiveFunc, Uri uri, SendReceiveType sendReceiveTestType) + { + SendReceiveTestType = sendReceiveTestType; + return sendReceiveFunc(uri); + } - protected override Task SendAsync(WebSocket ws, ArraySegment arraySegment, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) => - ws.SendAsync(arraySegment, messageType, endOfMessage, cancellationToken); - } + #endregion - public abstract class SendReceiveTestBase(ITestOutputHelper output) : ClientWebSocketTestBase(output) - { - protected abstract Task SendAsync(WebSocket ws, ArraySegment arraySegment, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken); - protected abstract Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken); + #region Common (Echo Server) tests protected async Task RunClient_SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(Uri server) { @@ -260,7 +278,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) { @@ -386,72 +404,7 @@ protected async Task RunClient_SendReceive_Concurrent_Success(Uri server) } } - protected 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); - } protected async Task RunClient_ZeroByteReceive_CompletesWhenDataAvailable(Uri server) { @@ -476,7 +429,7 @@ protected async Task RunClient_ZeroByteReceive_CompletesWhenDataAvailable(Uri se 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); } @@ -491,53 +444,70 @@ protected async Task RunClient_ZeroByteReceive_CompletesWhenDataAvailable(Uri se 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(ITestOutputHelper output) : SendReceiveTestBase(output) + public abstract class SendReceiveTest_External(ITestOutputHelper output) : SendReceiveTestBase(output) { - [Theory, MemberData(nameof(EchoServers))] - public Task SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(Uri server) - => RunClient_SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(server); + #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(EchoServers))] - public Task SendReceive_PartialMessageBeforeCompleteMessageArrives_Success(Uri server) - => RunClient_SendReceive_PartialMessageBeforeCompleteMessageArrives_Success(server); + [Theory, MemberData(nameof(EchoServersAndSendReceiveType))] + public Task SendReceive_PartialMessageBeforeCompleteMessageArrives_Success(Uri server, SendReceiveType type) => RunSendReceive( + RunClient_SendReceive_PartialMessageBeforeCompleteMessageArrives_Success, server, type); - [Theory, MemberData(nameof(EchoServers))] - public Task SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage(Uri server) - => RunClient_SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage(server); + [Theory, MemberData(nameof(EchoServersAndSendReceiveType))] + public Task SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage(Uri server, SendReceiveType type) => RunSendReceive( + RunClient_SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage, server, type); - [Theory, MemberData(nameof(EchoServers))] - public Task SendAsync_MultipleOutstandingSendOperations_Throws(Uri server) - => RunClient_SendAsync_MultipleOutstandingSendOperations_Throws(server); + [Theory, MemberData(nameof(EchoServersAndSendReceiveType))] + public Task SendAsync_MultipleOutstandingSendOperations_Throws(Uri server, SendReceiveType type) => RunSendReceive( + RunClient_SendAsync_MultipleOutstandingSendOperations_Throws, server, type); [ActiveIssue("https://github.com/dotnet/runtime/issues/83517", typeof(PlatformDetection), nameof(PlatformDetection.IsNodeJS))] - [Theory, MemberData(nameof(EchoServers))] - public Task ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(Uri server) - => RunClient_ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(server); - - [Theory, MemberData(nameof(EchoServers))] - public Task SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(Uri server) - => RunClient_SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(server); - - [Theory, MemberData(nameof(EchoServers))] - public Task SendReceive_VaryingLengthBuffers_Success(Uri server) - => RunClient_SendReceive_VaryingLengthBuffers_Success(server); - - [Theory, MemberData(nameof(EchoServers))] - public Task SendReceive_Concurrent_Success(Uri server) - => RunClient_SendReceive_Concurrent_Success(server); - - [ActiveIssue("https://github.com/dotnet/runtime/issues/54153", TestPlatforms.Browser)] - [Fact] - public Task SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated() - => RunClient_SendReceive_ConnectionClosedPrematurely_ReceiveAsyncFailsAndWebSocketStateUpdated(); - - [Theory, MemberData(nameof(EchoServers))] - public Task ZeroByteReceive_CompletesWhenDataAvailable(Uri server) - => RunClient_ZeroByteReceive_CompletesWhenDataAvailable(server); + [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 c09f9bbae9683f..f9f32c2e86ad87 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 @@ -65,8 +65,9 @@ - + + @@ -81,11 +82,15 @@ + + + + diff --git a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs index b6fe5c88d2df8c..b3e7d96ba6defd 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs @@ -2,7 +2,6 @@ // 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; @@ -32,32 +31,31 @@ public static async Task TestEcho( var receiveBuffer = new byte[100]; var receiveSegment = new ArraySegment(receiveBuffer); - using (ClientWebSocket cws = await GetConnectedWebSocket(server, timeOutMilliseconds, output, configureOptions, invoker)) - { - await cws.SendAsync(WebSocketData.GetBufferFromText(message), type, true, cts.Token); - Assert.Equal(WebSocketState.Open, cws.State); - - WebSocketReceiveResult recvRet = await cws.ReceiveAsync(receiveSegment, cts.Token); - 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)); - - 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; - Assert.Equal(WebSocketState.Closed, cws.State); - Assert.Equal(WebSocketCloseStatus.NormalClosure, cws.CloseStatus); - Assert.Equal(closeMessage, cws.CloseStatusDescription); - } + using ClientWebSocket cws = await GetConnectedWebSocket(server, timeOutMilliseconds, output, configureOptions, invoker); + + await cws.SendAsync(WebSocketData.GetBufferFromText(message), type, true, cts.Token); + Assert.Equal(WebSocketState.Open, cws.State); + + WebSocketReceiveResult recvRet = await cws.ReceiveAsync(receiveSegment, cts.Token); + 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)); + + 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; + Assert.Equal(WebSocketState.Closed, cws.State); + Assert.Equal(WebSocketCloseStatus.NormalClosure, cws.CloseStatus); + Assert.Equal(closeMessage, cws.CloseStatusDescription); } public static Task GetConnectedWebSocket( @@ -94,26 +92,47 @@ public static Task GetConnectedWebSocket( Retry(output, async () => { var cws = new ClientWebSocket(); - configureOptions(cws.Options); - if (PlatformDetection.IsNotBrowser && invoker == null && server.Scheme == "wss") + using var cts = new CancellationTokenSource(timeOutMilliseconds); + await ConnectAsync(cws, server, configureOptions, invoker, validateState: true, cts.Token); + return cws; + }); + + public static async Task ConnectAsync( + ClientWebSocket cws, + Uri server, + Action configureOptions, + HttpMessageInvoker? invoker = null, + bool validateState = true, + CancellationToken cancellationToken = default) + { + if (PlatformDetection.IsNotBrowser && server.Scheme == "wss" && invoker == null) { - cws.Options.RemoteCertificateValidationCallback ??= (_, _, _, _) => true; + cws.Options.RemoteCertificateValidationCallback = (_, _, _, _) => true; } - using (var cts = new CancellationTokenSource(timeOutMilliseconds)) + configureOptions(cws.Options); + + Task taskConnect = invoker == null + ? cws.ConnectAsync(server, cancellationToken) + : cws.ConnectAsync(server, invoker, cancellationToken); + + if (validateState) { - 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; + } + + await taskConnect; + + if (validateState) + { Assert.Equal(WebSocketState.Open, cws.State); } - return cws; - }); + } public static async Task Retry(ITestOutputHelper output, Func> func) { 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 From 575e18eb2ecd9f19d186a4740a406821ab36f7c8 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Wed, 2 Jul 2025 23:46:59 +0100 Subject: [PATCH 14/20] Fix external send-receive --- .../tests/SendReceiveTest.cs | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs index df576cb84cb980..b06e290c48af71 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs @@ -31,7 +31,11 @@ namespace System.Net.WebSockets.Client.Tests public abstract class SendReceiveTestBase(ITestOutputHelper output) : ClientWebSocketTestBase(output) { - #region Send-receive type test setup + #region Send-receive type setup + + 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()); public enum SendReceiveType { @@ -39,43 +43,40 @@ public enum SendReceiveType Memory } - public static readonly object[][] EchoServersAndSendReceiveType = ToMemberData(EchoServers_Values, Enum.GetValues()); + protected Func, WebSocketMessageType, bool, CancellationToken, Task> SendAsync { get; private set; } = null!; + protected Func, CancellationToken, Task> ReceiveAsync { get; private set; } = null!; - public static readonly object[][] UseSslAndSendReceiveType = ToMemberData(UseSsl_Values, Enum.GetValues()); - public static readonly object[][] SendReceiveTypes = ToMemberData(Enum.GetValues()); - - protected SendReceiveType? SendReceiveTestType { get; set; } - - protected Task SendAsync(WebSocket ws, ArraySegment arraySegment, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) - => SendReceiveTestType switch + private void SelectSendReceive(SendReceiveType sendReceiveTestType) + { + SendAsync = sendReceiveTestType switch { - SendReceiveType.ArraySegment => ws.SendAsync(arraySegment, messageType, endOfMessage, cancellationToken), - SendReceiveType.Memory => ws.SendAsync((ReadOnlyMemory)arraySegment, messageType, endOfMessage, cancellationToken).AsTask(), - _ => throw new ArgumentException(nameof(SendReceiveTestType)) + SendReceiveType.ArraySegment => static (ws, buf, opcode, eom, ct) => ws.SendAsync(buf, opcode, eom, ct), + SendReceiveType.Memory => static (ws, buf, opcode, eom, ct) => ws.SendAsync((ReadOnlyMemory)buf, opcode, eom, ct).AsTask(), + _ => throw new ArgumentException(nameof(sendReceiveTestType)) }; - protected Task ReceiveAsync(WebSocket ws, ArraySegment arraySegment, CancellationToken cancellationToken) - => SendReceiveTestType switch + ReceiveAsync = sendReceiveTestType switch { - SendReceiveType.ArraySegment => ws.ReceiveAsync(arraySegment, cancellationToken), - SendReceiveType.Memory => Task.Run(async () => + SendReceiveType.ArraySegment => static (ws, buf, ct) => ws.ReceiveAsync(buf, ct), + SendReceiveType.Memory => static async (ws, buf, ct) => { - ValueWebSocketReceiveResult r = await ws.ReceiveAsync((Memory)arraySegment, cancellationToken).ConfigureAwait(false); + ValueWebSocketReceiveResult r = await ws.ReceiveAsync((Memory)buf, ct).ConfigureAwait(false); return new WebSocketReceiveResult(r.Count, r.MessageType, r.EndOfMessage, ws.CloseStatus, ws.CloseStatusDescription); - }), - _ => throw new ArgumentException(nameof(SendReceiveTestType)) + }, + _ => throw new ArgumentException(nameof(sendReceiveTestType)) }; + } protected Task RunSendReceive(Func sendReceiveFunc, SendReceiveType sendReceiveTestType) { - SendReceiveTestType = sendReceiveTestType; + SelectSendReceive(sendReceiveTestType); return sendReceiveFunc(); } protected Task RunSendReceive(Func sendReceiveFunc, Uri uri, SendReceiveType sendReceiveTestType) { - SendReceiveTestType = sendReceiveTestType; + SelectSendReceive(sendReceiveTestType); return sendReceiveFunc(uri); } @@ -404,8 +405,6 @@ protected async Task RunClient_SendReceive_Concurrent_Success(Uri server) } } - - protected async Task RunClient_ZeroByteReceive_CompletesWhenDataAvailable(Uri server) { using (ClientWebSocket cws = await GetConnectedWebSocket(server)) @@ -495,7 +494,7 @@ public Task ZeroByteReceive_CompletesWhenDataAvailable(Uri server, SendReceiveTy #endregion } - /*#region Runnable test classes: External/Outerloop + #region Runnable test classes: External/Outerloop public sealed class SendReceiveTest_SharedHandler_External(ITestOutputHelper output) : SendReceiveTest_External(output) { } @@ -509,5 +508,5 @@ public sealed class SendReceiveTest_HttpClient_External(ITestOutputHelper output protected override bool UseHttpClient => true; } - #endregion*/ + #endregion } From eb9771ee747858023532ab3ad6cc5baa5b2bab81 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Thu, 3 Jul 2025 00:12:57 +0100 Subject: [PATCH 15/20] Fixed most of the loopback send-receive --- .../tests/SendReceiveTest.Loopback.cs | 12 ++++++------ .../tests/SendReceiveTest.cs | 19 +++++++++++++++++-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs index bd09d929bb02c2..f54a4202d099d1 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.Loopback.cs @@ -22,7 +22,7 @@ public abstract class SendReceiveTest_LoopbackBase(ITestOutputHelper output) : S public Task SendReceive_PartialMessageDueToSmallReceiveBuffer_Success(bool useSsl, SendReceiveType type) => RunEchoAsync( server => RunSendReceive(RunClient_SendReceive_PartialMessageDueToSmallReceiveBuffer_Success, server, type), useSsl); - [Theory, MemberData(nameof(UseSslAndSendReceiveType))] + [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); @@ -42,7 +42,7 @@ public Task ReceiveAsync_MultipleOutstandingReceiveOperations_Throws(bool useSsl public Task SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success(bool useSsl, SendReceiveType type) => RunEchoAsync( server => RunSendReceive(RunClient_SendAsync_SendZeroLengthPayloadAsEndOfMessage_Success, server, type), useSsl); - [Theory, MemberData(nameof(UseSslAndSendReceiveType))] + [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); @@ -142,7 +142,7 @@ public abstract partial class SendReceiveTest_Http2Loopback(ITestOutputHelper ou // #region HTTP/2-only loopback tests -> extracted to SendReceiveTest.Http2.cs } - /*#region Runnable test classes: HTTP/1.1 Loopback + #region Runnable test classes: HTTP/1.1 Loopback public sealed class SendReceiveTest_SharedHandler_Loopback(ITestOutputHelper output) : SendReceiveTest_Loopback(output) { } @@ -156,9 +156,9 @@ public sealed class SendReceiveTest_HttpClient_Loopback(ITestOutputHelper output protected override bool UseHttpClient => true; } - #endregion*/ + #endregion - /*#region Runnable test classes: HTTP/2 Loopback + #region Runnable test classes: HTTP/2 Loopback public sealed class SendReceiveTest_Invoker_Http2Loopback(ITestOutputHelper output) : SendReceiveTest_Http2Loopback(output) { @@ -170,5 +170,5 @@ public sealed class SendReceiveTest_HttpClient_Http2Loopback(ITestOutputHelper o protected override bool UseHttpClient => true; } - #endregion*/ + #endregion } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs index b06e290c48af71..585650b00d9653 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs @@ -3,7 +3,7 @@ using System.Threading; using System.Threading.Tasks; - +using Microsoft.DotNet.XUnitExtensions; using Xunit; using Xunit.Abstractions; @@ -121,6 +121,11 @@ protected async Task RunClient_SendReceive_PartialMessageDueToSmallReceiveBuffer 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); @@ -241,11 +246,14 @@ protected async Task RunClient_SendAsync_MultipleOutstandingSendOperations_Throw } } + 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)) { - var cts = new CancellationTokenSource(PlatformDetection.LocalEchoServerIsNotAvailable ? TimeOutMilliseconds : 200); + var cts = new CancellationTokenSource(MultipleOutstandingReceiveOperations_TimeoutMs); Task[] tasks = new Task[2]; @@ -342,6 +350,11 @@ await SendAsync( protected async Task RunClient_SendReceive_VaryingLengthBuffers_Success(Uri server) { + if (HttpVersion == Net.HttpVersion.Version20) + { + throw new SkipTestException("[ActiveIssue] -- temporarily skipping on HTTP/2"); + } + using (ClientWebSocket cws = await GetConnectedWebSocket(server)) { var rand = new Random(); @@ -470,6 +483,8 @@ public Task SendAsync_SendCloseMessageType_ThrowsArgumentExceptionWithMessage(Ur 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( From 209a621bac5eb15eb7d22abf5205192d20277d37 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Thu, 3 Jul 2025 00:47:58 +0100 Subject: [PATCH 16/20] Removed WebSocketData --- .../tests/AbortTest.cs | 8 +-- .../tests/CancelTest.cs | 8 +-- .../tests/ClientWebSocketTestBase.cs | 5 +- .../tests/CloseTest.Loopback.cs | 13 +++- .../tests/CloseTest.cs | 19 +++--- .../tests/ConnectTest.cs | 4 +- .../tests/SendReceiveTest.cs | 14 +++-- .../System.Net.WebSockets.Client.Tests.csproj | 1 - .../tests/WebSocketData.cs | 21 ------- .../tests/WebSocketHelper.cs | 60 +++++++++++-------- ...em.Net.WebSockets.Client.Wasm.Tests.csproj | 1 - 11 files changed, 78 insertions(+), 76 deletions(-) delete mode 100644 src/libraries/System.Net.WebSockets.Client/tests/WebSocketData.cs diff --git a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs index 5603236d331114..d11edc6b40a949 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs @@ -64,7 +64,7 @@ await TestCancellation(async (cws) => var cts = new CancellationTokenSource(TimeOutMilliseconds); Task t = cws.SendAsync( - WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), + ToUtf8(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, cts.Token); @@ -82,7 +82,7 @@ await TestCancellation(async (cws) => var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), + ToUtf8(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -105,7 +105,7 @@ await TestCancellation(async (cws) => var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), + ToUtf8(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -127,7 +127,7 @@ await TestCancellation(async (cws) => var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), + ToUtf8(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, ctsDefault.Token); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs index 5954eee8ca4561..f2e7bfcf46c7d6 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs @@ -58,7 +58,7 @@ await TestCancellation((cws) => { var cts = new CancellationTokenSource(5); return cws.SendAsync( - WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), + ToUtf8(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, cts.Token); @@ -73,7 +73,7 @@ await TestCancellation(async (cws) => var cts = new CancellationTokenSource(5); await cws.SendAsync( - WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), + ToUtf8(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -93,7 +93,7 @@ await TestCancellation(async (cws) => var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), + ToUtf8(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -114,7 +114,7 @@ await TestCancellation(async (cws) => var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(EchoControlMessage.Delay5Sec), + ToUtf8(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, ctsDefault.Token); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs index 28a8ab88b20bb1..e42e22f4778e22 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs @@ -39,6 +39,9 @@ public static object[][] ToMemberData(IEnumerable dataA, IEnumer public const int CloseDescriptionMaxLength = 123; public readonly ITestOutputHelper _output = output; + public static ArraySegment ToUtf8(string text) => WebSocketHelper.ToUtf8(text); + public static string FromUtf8(ArraySegment buffer) => WebSocketHelper.FromUtf8(buffer); + public static IEnumerable UnavailableWebSocketServers { get @@ -58,7 +61,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 }; } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs index f11d7982724268..33b42133f13f52 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs @@ -50,18 +50,25 @@ public Task CloseOutputAsync_ExpectedStates(bool useSsl) => RunEchoAsync( 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(UseSslAndBoolean))] - public Task CloseOutputAsync_ServerInitiated_CanReceive(bool useSsl, bool delayReceiving) => RunEchoAsync( - server => RunClient_CloseOutputAsync_ServerInitiated_CanReceive(server, delayReceiving), 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); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs index efa31fbe8cbf23..e6822779fe62bd 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs @@ -43,7 +43,7 @@ protected async Task RunClient_CloseAsync_ServerInitiatedClose_Success(Uri serve var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(EchoControlMessage.Shutdown), + ToUtf8(EchoControlMessage.Shutdown), WebSocketMessageType.Text, true, cts.Token); @@ -212,7 +212,7 @@ protected async Task RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanCl 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(ToUtf8(message), 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 @@ -226,7 +226,7 @@ protected async Task RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanCl 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, FromUtf8(segmentRecv)); Assert.Null(recvResult.CloseStatus); Assert.Null(recvResult.CloseStatusDescription); @@ -247,7 +247,7 @@ protected async Task RunClient_CloseOutputAsync_ServerInitiated_CanReceive(Uri s var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(expectedCloseDescription), + ToUtf8(expectedCloseDescription), WebSocketMessageType.Text, true, cts.Token); @@ -262,7 +262,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, FromUtf8(segmentRecv)); Assert.Null(recvResult.CloseStatus); Assert.Null(recvResult.CloseStatusDescription); @@ -302,7 +302,7 @@ protected async Task RunClient_CloseOutputAsync_ServerInitiated_CanSend(Uri serv var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(EchoControlMessage.Shutdown), + ToUtf8(EchoControlMessage.Shutdown), WebSocketMessageType.Text, true, cts.Token); @@ -322,7 +322,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(ToUtf8(message), WebSocketMessageType.Text, true, cts.Token); // Cannot change the close status/description with the final close. var closeStatus = PlatformDetection.IsNotBrowser ? WebSocketCloseStatus.InvalidPayloadData : (WebSocketCloseStatus)3210; @@ -342,7 +342,7 @@ protected async Task RunClient_CloseOutputAsync_ServerInitiated_CanReceiveAfterC { var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - WebSocketData.GetBufferFromText(EchoControlMessage.ReceiveMessageAfterClose), + ToUtf8(EchoControlMessage.ReceiveMessageAfterClose), WebSocketMessageType.Text, true, cts.Token); @@ -358,7 +358,8 @@ 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 = FromUtf8(recvSegment); Assert.Contains(EchoControlMessage.ReceiveMessageAfterClose, message); } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs index 384e4804d7e1d5..102ef19a3ecdc3 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs @@ -75,7 +75,7 @@ protected async Task RunClient_ConnectAsync_AddCustomHeaders_Success(Uri server) } Assert.Equal(WebSocketMessageType.Text, recvResult.MessageType); - string headers = WebSocketData.GetTextFromBuffer(new ArraySegment(buffer, 0, recvResult.Count)); + string headers = FromUtf8(new ArraySegment(buffer, 0, recvResult.Count)); Assert.Contains("X-CustomHeader1:Value1", headers, StringComparison.OrdinalIgnoreCase); Assert.Contains("X-CustomHeader2:Value2", headers, StringComparison.OrdinalIgnoreCase); @@ -129,7 +129,7 @@ protected async Task RunClient_ConnectAsync_CookieHeaders_Success(Uri server) } Assert.Equal(WebSocketMessageType.Text, recvResult.MessageType); - string headers = WebSocketData.GetTextFromBuffer(new ArraySegment(buffer, 0, recvResult.Count)); + string headers = FromUtf8(new ArraySegment(buffer, 0, recvResult.Count)); Assert.Contains("Cookies=Are Yummy", headers); Assert.Contains("Especially=Chocolate Chip", headers); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs index 585650b00d9653..4462890d3cff4c 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs @@ -7,6 +7,9 @@ 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 { // @@ -131,8 +134,7 @@ protected async Task RunClient_SendReceive_PartialMessageBeforeCompleteMessageAr 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 GetConnectedWebSocket(ub.Uri)) { @@ -203,7 +205,7 @@ protected async Task RunClient_SendAsync_MultipleOutstandingSendOperations_Throw { tasks[i] = SendAsync( cws, - WebSocketData.GetBufferFromText("hello"), + ToUtf8("hello"), WebSocketMessageType.Text, true, cts.Token); @@ -259,7 +261,7 @@ protected async Task RunClient_ReceiveAsync_MultipleOutstandingReceiveOperations await SendAsync( cws, - WebSocketData.GetBufferFromText(".delay5sec"), + ToUtf8(EchoControlMessage.Delay5Sec), WebSocketMessageType.Text, true, cts.Token); @@ -319,7 +321,7 @@ protected async Task RunClient_SendAsync_SendZeroLengthPayloadAsEndOfMessage_Suc string message = "hello"; await SendAsync( cws, - WebSocketData.GetBufferFromText(message), + ToUtf8(message), WebSocketMessageType.Text, false, cts.Token); @@ -344,7 +346,7 @@ 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, FromUtf8(recvSegment)); } } 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 f9f32c2e86ad87..1d19541481c6ba 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 @@ -84,7 +84,6 @@ - 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 b3e7d96ba6defd..061ff33ee5f189 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Net.Http; +using System.Text; using System.Threading; using System.Threading.Tasks; @@ -33,7 +34,7 @@ public static async Task TestEcho( using ClientWebSocket cws = await GetConnectedWebSocket(server, timeOutMilliseconds, output, configureOptions, invoker); - await cws.SendAsync(WebSocketData.GetBufferFromText(message), type, true, cts.Token); + await cws.SendAsync(ToUtf8(message), type, true, cts.Token); Assert.Equal(WebSocketState.Open, cws.State); WebSocketReceiveResult recvRet = await cws.ReceiveAsync(receiveSegment, cts.Token); @@ -45,7 +46,7 @@ public static async Task TestEcho( Assert.Null(recvRet.CloseStatusDescription); var recvSegment = new ArraySegment(receiveSegment.Array, receiveSegment.Offset, recvRet.Count); - Assert.Equal(message, WebSocketData.GetTextFromBuffer(recvSegment)); + Assert.Equal(message, FromUtf8(recvSegment)); Task taskClose = cws.CloseAsync(WebSocketCloseStatus.NormalClosure, closeMessage, cts.Token); Assert.True( @@ -104,35 +105,35 @@ public static async Task ConnectAsync( HttpMessageInvoker? invoker = null, bool validateState = true, CancellationToken cancellationToken = default) + { + if (PlatformDetection.IsNotBrowser && server.Scheme == "wss" && invoker == null) { - if (PlatformDetection.IsNotBrowser && server.Scheme == "wss" && invoker == null) - { - cws.Options.RemoteCertificateValidationCallback = (_, _, _, _) => true; - } + cws.Options.RemoteCertificateValidationCallback = (_, _, _, _) => true; + } - configureOptions(cws.Options); + configureOptions(cws.Options); - Task taskConnect = invoker == null - ? cws.ConnectAsync(server, cancellationToken) - : cws.ConnectAsync(server, invoker, cancellationToken); + Task taskConnect = invoker == null + ? cws.ConnectAsync(server, cancellationToken) + : cws.ConnectAsync(server, invoker, cancellationToken); - if (validateState) - { - 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); - } + if (validateState) + { + 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; + await taskConnect; - if (validateState) - { - Assert.Equal(WebSocketState.Open, cws.State); - } + if (validateState) + { + Assert.Equal(WebSocketState.Open, cws.State); } + } public static async Task Retry(ITestOutputHelper output, Func> func) { @@ -184,5 +185,16 @@ private static bool InitWebSocketSupported() } } } + + public static ArraySegment ToUtf8(string text) + { + byte[] buffer = Encoding.UTF8.GetBytes(text); + return new ArraySegment(buffer); + } + + public static string FromUtf8(ArraySegment buffer) + { + return Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count); + } } } 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 @@ - From 8587c86645bb19b1768304830175ffdb04ad2c6e Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Thu, 3 Jul 2025 13:51:17 +0100 Subject: [PATCH 17/20] NetCoreServer fix, removed unused stuff --- .../Common/tests/System/Net/Configuration.WebSockets.cs | 2 -- .../Common/tests/System/Net/Http/GenericLoopbackServer.cs | 1 - .../tests/System/Net/Http/Http2LoopbackConnection.cs | 8 ++++---- .../Prerequisites/NetCoreServer/Directory.Build.targets | 1 + .../NetCoreServer/Handlers/EchoWebSocketHandler.cs | 3 +-- .../NetCoreServer/Handlers/EchoWebSocketHeadersHandler.cs | 5 +---- .../NetCoreServer/Helpers/WebSocketAcceptHelper.cs | 5 ++--- .../Net/Prerequisites/NetCoreServer/NetCoreServer.csproj | 6 +++--- .../Helpers => WebSockets}/WebSocketEchoHelper.cs | 0 .../Helpers => WebSockets}/WebSocketEchoOptions.cs | 0 .../System.Net.WebSockets.Client/tests/CloseTest.cs | 1 - .../System.Net.WebSockets.Client/tests/DeflateTests.cs | 1 - .../System.Net.WebSockets.Client/tests/KeepAliveTest.cs | 2 -- .../System.Net.WebSockets.Client/tests/ResourceHelper.cs | 1 - .../tests/System.Net.WebSockets.Client.Tests.csproj | 4 ++-- 15 files changed, 14 insertions(+), 26 deletions(-) rename src/libraries/Common/tests/System/Net/{Prerequisites/NetCoreServer/Helpers => WebSockets}/WebSocketEchoHelper.cs (100%) rename src/libraries/Common/tests/System/Net/{Prerequisites/NetCoreServer/Helpers => WebSockets}/WebSocketEchoOptions.cs (100%) diff --git a/src/libraries/Common/tests/System/Net/Configuration.WebSockets.cs b/src/libraries/Common/tests/System/Net/Configuration.WebSockets.cs index 87ea91a0c07af9..fcd4f9faf743db 100644 --- a/src/libraries/Common/tests/System/Net/Configuration.WebSockets.cs +++ b/src/libraries/Common/tests/System/Net/Configuration.WebSockets.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.Linq; - namespace System.Net.Test.Common { public static partial class Configuration diff --git a/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs index 114bfa21332a57..93a9c6318b601e 100644 --- a/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs @@ -188,7 +188,6 @@ public class GenericLoopbackOptions #if !NETSTANDARD2_0 && !NETFRAMEWORK public SslStreamCertificateContext? CertificateContext { get; set; } #endif - public TextWriter? Logger { get; set; } } public struct HttpHeaderData diff --git a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs index 0b3dae175766f3..5e268d5b93573d 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs @@ -61,7 +61,7 @@ static Stream CreateConcurrentConnectionStream(Stream stream, SemaphoreSlim read await readLock.WaitAsync(cancellationToken); try { - return await stream.ReadAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); + return await stream.ReadAsync(buffer, offset, count, cancellationToken); } finally { @@ -70,11 +70,11 @@ static Stream CreateConcurrentConnectionStream(Stream stream, SemaphoreSlim read }, writeAsyncFunc: async (buffer, offset, count, cancellationToken) => { - await writeLock.WaitAsync(cancellationToken).ConfigureAwait(false); + await writeLock.WaitAsync(cancellationToken); try { - await stream.WriteAsync(buffer, offset, count, cancellationToken).ConfigureAwait(false); - await stream.FlushAsync(cancellationToken).ConfigureAwait(false); + await stream.WriteAsync(buffer, offset, count, cancellationToken); + await stream.FlushAsync(cancellationToken); } finally { 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 977c05810d1aea..7f8edd45dab44e 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 @@ -4,8 +4,6 @@ using System; using System.Net.WebSockets; using System.Net.Test.Common; -using System.Text; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -15,6 +13,7 @@ public class EchoWebSocketHandler { public static async Task InvokeAsync(HttpContext context) { + var queryString = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : ""; WebSocketEchoOptions options = await WebSocketEchoHelper.ProcessOptions(queryString); try { 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 51ced55c384258..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 @@ -6,11 +6,8 @@ using System.Linq; using System.Net.WebSockets; using System.Net.Test.Common; -using System.Text; -using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; namespace NetCoreServer { @@ -27,7 +24,7 @@ public static async Task InvokeAsync(HttpContext context) } var headers = context.Request.Headers.Select( - h => new KeyValuePair(h.Key, h.Value.ToString())) + h => new KeyValuePair(h.Key, h.Value.ToString())); await WebSocketEchoHelper.RunEchoHeaders(socket, headers); } 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 index 713071691047cf..4476a2a928ac28 100644 --- a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketAcceptHelper.cs +++ b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketAcceptHelper.cs @@ -1,10 +1,9 @@ // 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.Text; -using System.Threading; +using System.Net.WebSockets; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; namespace NetCoreServer { 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 9f693201dd831a..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 @@ + + @@ -52,8 +54,6 @@ - - diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs b/src/libraries/Common/tests/System/Net/WebSockets/WebSocketEchoHelper.cs similarity index 100% rename from src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoHelper.cs rename to src/libraries/Common/tests/System/Net/WebSockets/WebSocketEchoHelper.cs diff --git a/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs b/src/libraries/Common/tests/System/Net/WebSockets/WebSocketEchoOptions.cs similarity index 100% rename from src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Helpers/WebSocketEchoOptions.cs rename to src/libraries/Common/tests/System/Net/WebSockets/WebSocketEchoOptions.cs diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs index e6822779fe62bd..ed8b1c62d63195 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.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.Text; using System.Threading; using System.Threading.Tasks; diff --git a/src/libraries/System.Net.WebSockets.Client/tests/DeflateTests.cs b/src/libraries/System.Net.WebSockets.Client/tests/DeflateTests.cs index 0c5e4977a4d5df..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; diff --git a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs index da4e2438a0b3ab..f9ec13945ceacd 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; 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/System.Net.WebSockets.Client.Tests.csproj b/src/libraries/System.Net.WebSockets.Client/tests/System.Net.WebSockets.Client.Tests.csproj index 1d19541481c6ba..5cd3e0acfc310c 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 @@ -49,8 +49,8 @@ - - + + From b364e9de4687b9b38bd43dd03e462ffee2a4a0d7 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Fri, 4 Jul 2025 14:36:07 +0100 Subject: [PATCH 18/20] Fix browser build --- .../ClientWebSocketTestBase.Echo.Unsupported.cs | 16 ++++++++++++++++ ...opback.cs => ClientWebSocketTestBase.Echo.cs} | 0 .../LoopbackServer/LoopbackWebSocketServer.cs | 2 -- .../System.Net.WebSockets.Client.Tests.csproj | 5 ++++- 4 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Echo.Unsupported.cs rename src/libraries/System.Net.WebSockets.Client/tests/{ClientWebSocketTestBase.Loopback.cs => ClientWebSocketTestBase.Echo.cs} (100%) 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.Loopback.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Echo.cs similarity index 100% rename from src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Loopback.cs rename to src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Echo.cs 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 df7d7e5fcd70fe..85184913158a42 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs @@ -1,10 +1,8 @@ // 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.Http; using System.Net.Test.Common; -using System.Text; using System.Threading; using System.Threading.Tasks; using Xunit; 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 5cd3e0acfc310c..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 @@ -88,7 +88,10 @@ - + + + + From 299be593e0d0e91ea7f1cc9c86faae7940c7453c Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Sat, 12 Jul 2025 12:41:38 +0100 Subject: [PATCH 19/20] remove ConfigureHttp2Options --- .../tests/KeepAliveTest.Loopback.cs | 1 - .../tests/LoopbackServer/LoopbackWebSocketServer.Http.cs | 3 +-- .../tests/LoopbackServer/LoopbackWebSocketServer.cs | 5 ++--- 3 files changed, 3 insertions(+), 6 deletions(-) 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 d1042e6c698c89..4ab8a1865a0c04 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs @@ -54,7 +54,6 @@ public Task KeepAlive_LongDelayBetweenSendReceives_Succeeds(bool useSsl) clientOptions.KeepAliveInterval = TimeSpan.FromMilliseconds(100); clientOptions.KeepAliveTimeout = TimeSpan.FromSeconds(1); }, - ConfigureHttp2Options = http2Options => http2Options.EnsureThreadSafeIO = true, Output = _output }; 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 index d2239f9d61ac2a..a00a1775c860ba 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs @@ -12,7 +12,7 @@ namespace System.Net.WebSockets.Client.Tests { public static partial class LoopbackWebSocketServer { - private static Task RunClientAndServerAsync( + private static Task RunClientAndHttpServerAsync( Func clientFunc, Func loopbackServerFunc, Options options, @@ -31,7 +31,6 @@ private static Task RunClientAndServerAsync( if (options.HttpVersion == HttpVersion.Version20) { var http2Options = new Http2Options { WebSocketEndpoint = true, UseSsl = options.UseSsl, EnsureThreadSafeIO = true }; - options.ConfigureHttp2Options?.Invoke(http2Options); return Http2LoopbackServer.CreateClientAndServerAsync( clientFunc, 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 85184913158a42..78d97f11f578d0 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.cs @@ -49,13 +49,13 @@ private static Task RunAsyncPrivate( { if (!options.AbortServerOnClientExit) { - return RunClientAndServerAsync( + return RunClientAndHttpServerAsync( loopbackClientFunc, loopbackServerFunc, options, CancellationToken.None, globalCt); } CancellationTokenSource clientExitCts = new CancellationTokenSource(); - return RunClientAndServerAsync( + return RunClientAndHttpServerAsync( async uri => { try @@ -145,7 +145,6 @@ public record class Options() public bool AbortServerOnClientExit { get; set; } public string? ServerSubProtocol { get; set; } public string? ServerExtensions { get; set; } - public Action? ConfigureHttp2Options { get; set; } public bool DisposeClientWebSocket { get; set; } public bool DisposeHttpInvoker { get; set; } From 646cb44d7250cf83305813170e29fa81428a21c1 Mon Sep 17 00:00:00 2001 From: Natalia Kondratyeva Date: Tue, 22 Jul 2025 16:00:44 +0100 Subject: [PATCH 20/20] Address PR feedback --- .../System/Net/Http/Http2LoopbackServer.cs | 7 +- .../tests/System/Net/Http/LoopbackServer.cs | 5 + .../Handlers/EchoWebSocketHandler.cs | 2 +- .../tests/AbortTest.Loopback.cs | 29 +--- .../tests/AbortTest.cs | 8 +- .../tests/CancelTest.cs | 8 +- .../tests/ClientWebSocketOptionsTests.cs | 8 +- .../tests/ClientWebSocketTestBase.Echo.cs | 14 +- .../tests/ClientWebSocketTestBase.cs | 82 ++++++--- .../tests/CloseTest.Loopback.cs | 6 +- .../tests/CloseTest.cs | 18 +- .../tests/ConnectTest.cs | 19 +-- .../tests/KeepAliveTest.Loopback.cs | 27 ++- .../tests/KeepAliveTest.cs | 6 +- .../LoopbackWebSocketServer.Echo.cs | 2 +- .../LoopbackWebSocketServer.Http.cs | 156 +++++++++--------- .../LoopbackServer/LoopbackWebSocketServer.cs | 122 ++++---------- .../tests/SendReceiveTest.cs | 57 ++++--- .../tests/WebSocketHelper.cs | 119 ++----------- .../tests/wasm/BrowserTimerThrottlingTest.cs | 2 +- 20 files changed, 279 insertions(+), 418 deletions(-) diff --git a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs index 39c8210bb68bcd..3e4c7584ad03ce 100644 --- a/src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs +++ b/src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs @@ -217,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/Handlers/EchoWebSocketHandler.cs b/src/libraries/Common/tests/System/Net/Prerequisites/NetCoreServer/Handlers/EchoWebSocketHandler.cs index 7f8edd45dab44e..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 @@ -13,7 +13,7 @@ public class EchoWebSocketHandler { public static async Task InvokeAsync(HttpContext context) { - var queryString = context.Request.QueryString.HasValue ? context.Request.QueryString.Value : ""; + var queryString = context.Request.QueryString.ToUriComponent(); // Returns empty string if request URI has no query WebSocketEchoOptions options = await WebSocketEchoHelper.ProcessOptions(queryString); try { 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 63e6f75b9fbcc1..1c7823434eb74e 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.Loopback.cs @@ -52,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) @@ -83,14 +85,7 @@ public Task AbortClient_ServerGetsCorrectException(AbortType abortType, bool use Assert.Equal(WebSocketError.ConnectionClosedPrematurely, exception.WebSocketErrorCode); Assert.Equal(WebSocketState.Aborted, serverWebSocket.State); }, - new LoopbackWebSocketServer.Options - { - HttpVersion = HttpVersion, - UseSsl = useSsl, - HttpInvoker = GetInvoker(), - DisposeServerWebSocket = true, - Output = _output - }, + new LoopbackWebSocketServer.Options(HttpVersion, useSsl) { DisposeServerWebSocket = true }, timeoutCts.Token); } @@ -107,15 +102,6 @@ public Task ServerPrematureEos_ClientGetsCorrectException(ServerEosType serverEo var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); - var globalOptions = new LoopbackWebSocketServer.Options - { - HttpVersion = HttpVersion, - UseSsl = useSsl, - HttpInvoker = null, - SkipServerHandshakeResponse = true, - Output = _output - }; - var serverReceivedEosTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var clientReceivedEosTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -123,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) { @@ -174,7 +159,7 @@ await SendServerResponseAndEosAsync( serverWebSocket.Dispose(); }, - globalOptions, + new LoopbackWebSocketServer.Options(HttpVersion, useSsl) { SkipServerHandshakeResponse = true }, timeoutCts.Token); } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs index d11edc6b40a949..703c7f2a1c477b 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/AbortTest.cs @@ -64,7 +64,7 @@ await TestCancellation(async (cws) => var cts = new CancellationTokenSource(TimeOutMilliseconds); Task t = cws.SendAsync( - ToUtf8(EchoControlMessage.Delay5Sec), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); @@ -82,7 +82,7 @@ await TestCancellation(async (cws) => var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - ToUtf8(EchoControlMessage.Delay5Sec), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -105,7 +105,7 @@ await TestCancellation(async (cws) => var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - ToUtf8(EchoControlMessage.Delay5Sec), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -127,7 +127,7 @@ await TestCancellation(async (cws) => var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - ToUtf8(EchoControlMessage.Delay5Sec), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, ctsDefault.Token); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs index f2e7bfcf46c7d6..20df47b2e6180b 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CancelTest.cs @@ -58,7 +58,7 @@ await TestCancellation((cws) => { var cts = new CancellationTokenSource(5); return cws.SendAsync( - ToUtf8(EchoControlMessage.Delay5Sec), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); @@ -73,7 +73,7 @@ await TestCancellation(async (cws) => var cts = new CancellationTokenSource(5); await cws.SendAsync( - ToUtf8(EchoControlMessage.Delay5Sec), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -93,7 +93,7 @@ await TestCancellation(async (cws) => var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - ToUtf8(EchoControlMessage.Delay5Sec), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, ctsDefault.Token); @@ -114,7 +114,7 @@ await TestCancellation(async (cws) => var ctsDefault = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - ToUtf8(EchoControlMessage.Delay5Sec), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, ctsDefault.Token); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs index cd37693a8fc994..7e64f6baa61246 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketOptionsTests.cs @@ -50,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; @@ -76,11 +76,7 @@ public async Task Proxy_ConnectThruProxy_Success(Uri server) _output.WriteLine($"ProxyServer: {proxyServerUri}"); IWebProxy proxy = new WebProxy(new Uri(proxyServerUri)); - using (ClientWebSocket cws = await WebSocketHelper.GetConnectedWebSocket( - server, - TimeOutMilliseconds, - _output, - proxy: proxy)) + using (ClientWebSocket cws = await GetConnectedWebSocket(server, o => o.Proxy = proxy)) { var cts = new CancellationTokenSource(TimeOutMilliseconds); Assert.Equal(WebSocketState.Open, cws.State); diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Echo.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Echo.cs index 0558d58b267113..c29aa596710b32 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Echo.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.Echo.cs @@ -12,15 +12,12 @@ public partial class ClientWebSocketTestBase protected Task RunEchoAsync(Func clientFunc, bool useSsl) { var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); - var options = new LoopbackWebSocketServer.Options + var options = new LoopbackWebSocketServer.Options(HttpVersion, useSsl) { - HttpVersion = HttpVersion, - UseSsl = useSsl, SkipServerHandshakeResponse = true, IgnoreServerErrors = true, AbortServerOnClientExit = true, - ParseEchoOptions = true, - Output = _output + ParseEchoOptions = true }; return LoopbackWebSocketServer.RunEchoAsync(clientFunc, options, timeoutCts.Token); @@ -29,13 +26,10 @@ protected Task RunEchoAsync(Func clientFunc, bool useSsl) protected Task RunEchoHeadersAsync(Func clientFunc, bool useSsl) { var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); - var options = new LoopbackWebSocketServer.Options + var options = new LoopbackWebSocketServer.Options(HttpVersion, useSsl) { - HttpVersion = HttpVersion, - UseSsl = useSsl, IgnoreServerErrors = true, - AbortServerOnClientExit = true, - Output = _output + AbortServerOnClientExit = true }; return LoopbackWebSocketServer.RunAsync( diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs index e42e22f4778e22..e1cd17fbaa3c4e 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ClientWebSocketTestBase.cs @@ -39,9 +39,6 @@ public static object[][] ToMemberData(IEnumerable dataA, IEnumer public const int CloseDescriptionMaxLength = 123; public readonly ITestOutputHelper _output = output; - public static ArraySegment ToUtf8(string text) => WebSocketHelper.ToUtf8(text); - public static string FromUtf8(ArraySegment buffer) => WebSocketHelper.FromUtf8(buffer); - public static IEnumerable UnavailableWebSocketServers { get @@ -141,35 +138,74 @@ protected static async Task ReceiveEntireMessageAsync(We return new HttpClient(handler); } - protected Task GetConnectedWebSocket(Uri uri) - => WebSocketHelper.GetConnectedWebSocket(uri, TimeOutMilliseconds, _output, o => ConfigureHttpVersion(o, uri), GetInvoker()); + public Task GetConnectedWebSocket(Uri uri, Action? configureOptions = null) + => WebSocketHelper.Retry( + async () => + { + var cws = new ClientWebSocket(); + configureOptions?.Invoke(cws.Options); - protected Task ConnectAsync(ClientWebSocket cws, Uri uri, CancellationToken cancellationToken) - => WebSocketHelper.ConnectAsync(cws, uri, o => ConfigureHttpVersion(o, uri), GetInvoker(), validateState: false, cancellationToken); + using var cts = new CancellationTokenSource(TimeOutMilliseconds); + Task taskConnect = ConnectAsync(cws, uri, cts.Token); - protected Task TestEcho(Uri uri, WebSocketMessageType type) - => WebSocketHelper.TestEcho(uri, type, TimeOutMilliseconds, _output, o => ConfigureHttpVersion(o, uri), GetInvoker()); + 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; - protected void ConfigureHttpVersion(ClientWebSocketOptions options, Uri uri) + Assert.Equal(WebSocketState.Open, cws.State); + return cws; + }); + + protected Task ConnectAsync(ClientWebSocket cws, Uri uri, CancellationToken cancellationToken) { - if (PlatformDetection.IsBrowser) + if (PlatformDetection.IsNotBrowser) { - return; - } + if (uri.Scheme == "wss" && UseSharedHandler) + { + cws.Options.RemoteCertificateValidationCallback = (_, _, _, _) => true; + } - options.HttpVersion = HttpVersion; - options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; + 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. + 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. - options.SetRequestHeader(WebSocketHelper.OriginalQueryStringHeader, uri.Query); + cws.Options.SetRequestHeader(WebSocketHelper.OriginalQueryStringHeader, uri.Query); + } } + + return UseSharedHandler + ? cws.ConnectAsync(uri, cancellationToken) // Ensure test coverage for both overloads + : 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 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 index 33b42133f13f52..66eb8a53715a20 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.Loopback.cs @@ -157,8 +157,10 @@ await RemoteExecutor.Invoke(static (typeName) => TaskCompletionSource clientCompleted = new TaskCompletionSource(); - return LoopbackWebSocketServer.RunAsync(async (clientWs, ct) => + 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); @@ -172,7 +174,7 @@ await RemoteExecutor.Invoke(static (typeName) => await serverWs.ReceiveAsync(new byte[16], ct); await serverWs.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, "", ct); await clientCompleted.Task; - }, new LoopbackWebSocketServer.Options { HttpVersion = Net.HttpVersion.Version11, UseSsl = true, HttpInvoker = test.GetInvoker() }, timeoutCts.Token); + }, new LoopbackWebSocketServer.Options(Net.HttpVersion.Version11, UseSsl: PlatformDetection.SupportsAlpn), timeoutCts.Token); }, GetType().FullName).DisposeAsync(); } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs index ed8b1c62d63195..9a8f33299de93e 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/CloseTest.cs @@ -42,7 +42,7 @@ protected async Task RunClient_CloseAsync_ServerInitiatedClose_Success(Uri serve var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - ToUtf8(EchoControlMessage.Shutdown), + EchoControlMessage.Shutdown.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); @@ -211,7 +211,7 @@ protected async Task RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanCl var closeStatus = PlatformDetection.IsNotBrowser ? WebSocketCloseStatus.InvalidPayloadData : (WebSocketCloseStatus)3210; string closeDescription = "CloseOutputAsync_Client_InvalidPayloadData"; - await cws.SendAsync(ToUtf8(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 @@ -225,7 +225,7 @@ protected async Task RunClient_CloseOutputAsync_ClientInitiated_CanReceive_CanCl 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, FromUtf8(segmentRecv)); + Assert.Equal(message, segmentRecv.Utf8ToString()); Assert.Null(recvResult.CloseStatus); Assert.Null(recvResult.CloseStatusDescription); @@ -246,7 +246,7 @@ protected async Task RunClient_CloseOutputAsync_ServerInitiated_CanReceive(Uri s var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - ToUtf8(expectedCloseDescription), + expectedCloseDescription.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); @@ -261,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, FromUtf8(segmentRecv)); + Assert.Equal(expectedCloseDescription, segmentRecv.Utf8ToString()); Assert.Null(recvResult.CloseStatus); Assert.Null(recvResult.CloseStatusDescription); @@ -301,7 +301,7 @@ protected async Task RunClient_CloseOutputAsync_ServerInitiated_CanSend(Uri serv var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - ToUtf8(EchoControlMessage.Shutdown), + EchoControlMessage.Shutdown.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); @@ -321,7 +321,7 @@ await cws.SendAsync( Assert.Equal(WebSocketState.CloseReceived, cws.State); // Should be able to send. - await cws.SendAsync(ToUtf8(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; @@ -341,7 +341,7 @@ protected async Task RunClient_CloseOutputAsync_ServerInitiated_CanReceiveAfterC { var cts = new CancellationTokenSource(TimeOutMilliseconds); await cws.SendAsync( - ToUtf8(EchoControlMessage.ReceiveMessageAfterClose), + EchoControlMessage.ReceiveMessageAfterClose.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); @@ -358,7 +358,7 @@ await cws.SendAsync( var recvBuffer = new ArraySegment(new byte[1024]); WebSocketReceiveResult recvResult = await cws.ReceiveAsync(recvBuffer, cts.Token); var recvSegment = new ArraySegment(recvBuffer.ToArray(), 0, recvResult.Count); - var message = FromUtf8(recvSegment); + var message = recvSegment.Utf8ToString(); Assert.Contains(EchoControlMessage.ReceiveMessageAfterClose, message); } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs index 102ef19a3ecdc3..01f0fc01138de1 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/ConnectTest.cs @@ -38,15 +38,14 @@ public abstract class ConnectTestBase(ITestOutputHelper output) : ClientWebSocke { #region Common (Echo Server) tests - protected async Task RunClient_EchoBinaryMessage_Success(Uri server) - { - await TestEcho(server, WebSocketMessageType.Binary); - } + protected Task RunClient_EchoBinaryMessage_Success(Uri server) + => RunClientAsync(server, + (cws, ct) => WebSocketHelper.TestEcho(cws, WebSocketMessageType.Binary, ct)); - protected async Task RunClient_EchoTextMessage_Success(Uri server) - { - await TestEcho(server, WebSocketMessageType.Text); - } + + protected Task RunClient_EchoTextMessage_Success(Uri server) + => RunClientAsync(server, + (cws, ct) => WebSocketHelper.TestEcho(cws, WebSocketMessageType.Text, ct)); protected async Task RunClient_ConnectAsync_AddCustomHeaders_Success(Uri server) { @@ -75,7 +74,7 @@ protected async Task RunClient_ConnectAsync_AddCustomHeaders_Success(Uri server) } Assert.Equal(WebSocketMessageType.Text, recvResult.MessageType); - string headers = FromUtf8(new ArraySegment(buffer, 0, recvResult.Count)); + 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); @@ -129,7 +128,7 @@ protected async Task RunClient_ConnectAsync_CookieHeaders_Success(Uri server) } Assert.Equal(WebSocketMessageType.Text, recvResult.MessageType); - string headers = FromUtf8(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); 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 4ab8a1865a0c04..767b4ddf8effae 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.Loopback.cs @@ -43,24 +43,17 @@ public Task KeepAlive_LongDelayBetweenSendReceives_Succeeds(bool useSsl) var timeoutCts = new CancellationTokenSource(TimeOutMilliseconds); - var options = new LoopbackWebSocketServer.Options - { - HttpVersion = HttpVersion, - UseSsl = useSsl, - HttpInvoker = GetInvoker(), - DisposeClientWebSocket = true, - ConfigureClientOptions = clientOptions => - { - clientOptions.KeepAliveInterval = TimeSpan.FromMilliseconds(100); - clientOptions.KeepAliveTimeout = TimeSpan.FromSeconds(1); - }, - Output = _output - }; - 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); @@ -98,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); } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs index f9ec13945ceacd..f9bea58936cdf5 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/KeepAliveTest.cs @@ -18,7 +18,7 @@ public class KeepAliveTest(ITestOutputHelper output) : ClientWebSocketTestBase(o [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); @@ -38,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/LoopbackServer/LoopbackWebSocketServer.Echo.cs b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Echo.cs index 5257b8f54ef817..d20e70cf1b8055 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Echo.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Echo.cs @@ -15,7 +15,7 @@ public static Task RunEchoAsync(Func loopbackClientFunc, Options opti Assert.True(options.IgnoreServerErrors); Assert.True(options.AbortServerOnClientExit); - return RunAsyncPrivate( + return RunAsync( loopbackClientFunc, (data, token) => RunEchoServerWebSocketAsync(data, options, token), options, 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 index a00a1775c860ba..42a2d5fbc7e33a 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/LoopbackServer/LoopbackWebSocketServer.Http.cs @@ -12,34 +12,92 @@ namespace System.Net.WebSockets.Client.Tests { public static partial class LoopbackWebSocketServer { - private static Task RunClientAndHttpServerAsync( - Func clientFunc, - Func loopbackServerFunc, - Options options, - CancellationToken clientExitCt, - CancellationToken globalCt) + abstract class HttpRunner { - if (options.HttpVersion == HttpVersion.Version11) + 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) { - return LoopbackServer.CreateClientAndServerAsync( - clientFunc, - server => RunHttpServer( - ProcessHttp11WebSocketRequest, server, loopbackServerFunc, options, clientExitCt, globalCt), - new LoopbackServer.Options { WebSocketEndpoint = true, UseSsl = options.UseSsl }); + 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); + } } - if (options.HttpVersion == HttpVersion.Version20) + private async Task ProcessWebSocketRequest( + GenericLoopbackServer httpServer, + Func wsServerFunc, + Options options, + CancellationToken clientExitCt, + CancellationToken globalCt) { - var http2Options = new Http2Options { WebSocketEndpoint = true, UseSsl = options.UseSsl, EnsureThreadSafeIO = true }; + 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 + } - return Http2LoopbackServer.CreateClientAndServerAsync( - clientFunc, - server => RunHttpServer( - ProcessHttp2WebSocketRequest, server, loopbackServerFunc, options, clientExitCt, globalCt), - http2Options); + throw; // don't swallow Assert failures and unexpected exceptions + } } - throw new ArgumentException(nameof(options.HttpVersion)); + 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( @@ -56,8 +114,8 @@ private static Task ProcessHttp11WebSocketRequest( options.ParseEchoOptions, cancellationToken).ConfigureAwait(false); - await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); - }); + await loopbackServerFunc(requestData, cancellationToken).ConfigureAwait(false); + }); private static async Task ProcessHttp2WebSocketRequest( Http2LoopbackServer http2Server, @@ -101,60 +159,6 @@ private static async Task ProcessHttp2WebSocketRequest( } } - private static async Task RunHttpServer( - Func, Options, CancellationToken, Task> httpServerFunc, - THttpServer httpServer, - Func wsServerFunc, - Options options, - CancellationToken clientExitCt, - CancellationToken globalCt) - where THttpServer : GenericLoopbackServer - { - try - { - using CancellationTokenSource linkedCts = - CancellationTokenSource.CreateLinkedTokenSource(globalCt, clientExitCt); - - await httpServerFunc(httpServer, wsServerFunc, options, linkedCts.Token) - .WaitAsync(linkedCts.Token).ConfigureAwait(false); - } - catch (Exception e) when (options.IgnoreServerErrors) - { - if (e is OperationCanceledException && clientExitCt.IsCancellationRequested) - { - return; // expected for aborting on client exit - } - - if (e is WebSocketException we) - { - if (we.WebSocketErrorCode == WebSocketError.ConnectionClosedPrematurely) - { - return; // expected for aborting on client exit - } - - if (we.WebSocketErrorCode == WebSocketError.InvalidState) - { - const string closeOnClosedMsg = "The WebSocket is in an invalid state ('Closed') for this operation. Valid states are: 'Open, CloseSent, CloseReceived'"; - const string closeOnAbortedMsg = "The WebSocket is in an invalid state ('Aborted') for this operation. Valid states are: 'Open, CloseSent, CloseReceived'"; - if (we.Message == closeOnClosedMsg || we.Message == closeOnAbortedMsg) - { - return; // expected (Close on a closed WebSocket is not no-op: see https://github.com/dotnet/runtime/issues/22000) - } - } - - options.Output?.WriteLine($"[WARN] Server aborted on a WebSocketException ({we.WebSocketErrorCode}): {we}"); - return; // ignore - } - - if (e is IOException or SocketException) - { - return; // ignore - } - - throw; // don't swallow Assert failures and unexpected exceptions - } - } - private static Task SendNegotiatedServerResponseAsync(WebSocketRequestData data, Options options, CancellationToken cancellationToken) { Assert.True(options.SkipServerHandshakeResponse); 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 78d97f11f578d0..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,76 +1,59 @@ // 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; -using Xunit.Abstractions; namespace System.Net.WebSockets.Client.Tests { public static partial class LoopbackWebSocketServer { public static Task RunAsync( - Func clientWebSocketFunc, - Func serverWebSocketFunc, + Func runClient, + Func runServer, Options options, CancellationToken cancellationToken) - { - Assert.False(options.SkipServerHandshakeResponse, "Required to create ClientWebSocket"); - - return RunAsyncPrivate( - uri => RunClientWebSocketAsync(uri, clientWebSocketFunc, options, cancellationToken), - (requestData, token) => RunServerWebSocketAsync(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, "ClientWebSocket is not created in this overload"); - Assert.False(options.DisposeServerWebSocket, "ServerWebSocket is not created in this overload"); - Assert.False(options.DisposeHttpInvoker, "HttpInvoker is not used 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 globalCt) - { - if (!options.AbortServerOnClientExit) + if (options.AbortServerOnClientExit) { - return RunClientAndHttpServerAsync( - loopbackClientFunc, loopbackServerFunc, options, CancellationToken.None, globalCt); - } - - CancellationTokenSource clientExitCts = new CancellationTokenSource(); + CancellationTokenSource clientExitCts = new CancellationTokenSource(); + clientExitCt = clientExitCts.Token; - return RunClientAndHttpServerAsync( - async uri => + var runClientCore = runClient; + runClient = async uri => { try { - await loopbackClientFunc(uri); + await runClientCore(uri); } finally { clientExitCts.Cancel(); } - }, - loopbackServerFunc, - options, - clientExitCts.Token, - globalCt); + }; + } + + var httpRunner = options.HttpVersion.Major switch + { + 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 RunServerWebSocketAsync( @@ -93,51 +76,8 @@ private static async Task RunServerWebSocketAsync( } } - private static async Task RunClientWebSocketAsync( - 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(); - } - - if (options.DisposeHttpInvoker) - { - options.HttpInvoker?.Dispose(); - } - } - - public static async Task GetConnectedClientAsync(Uri uri, Options options, CancellationToken cancellationToken) + public record class Options(Version HttpVersion, bool UseSsl) { - var clientWebSocket = new ClientWebSocket(); - clientWebSocket.Options.HttpVersion = options.HttpVersion; - clientWebSocket.Options.HttpVersionPolicy = HttpVersionPolicy.RequestVersionExact; - - if (options.UseSsl && options.HttpInvoker is null) - { - clientWebSocket.Options.RemoteCertificateValidationCallback = delegate { return true; }; - } - - options.ConfigureClientOptions?.Invoke(clientWebSocket.Options); - - await clientWebSocket.ConnectAsync(uri, options.HttpInvoker, cancellationToken).ConfigureAwait(false); - - return clientWebSocket; - } - - public record class Options() - { - public Version HttpVersion { get; init; } - public bool UseSsl { get; init; } - public HttpMessageInvoker? HttpInvoker { get; init; } - public bool DisposeServerWebSocket { get; set; } public bool SkipServerHandshakeResponse { get; set; } public bool ParseEchoOptions { get; set; } @@ -145,12 +85,6 @@ public record class Options() public bool AbortServerOnClientExit { get; set; } public string? ServerSubProtocol { get; set; } public string? ServerExtensions { get; set; } - - public bool DisposeClientWebSocket { get; set; } - public bool DisposeHttpInvoker { get; set; } - public Action? ConfigureClientOptions { get; set; } - - public ITestOutputHelper? Output { get; set; } } } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs index 4462890d3cff4c..f74e1efc848f48 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/SendReceiveTest.cs @@ -42,46 +42,49 @@ public abstract class SendReceiveTestBase(ITestOutputHelper output) : ClientWebS public enum SendReceiveType { - ArraySegment, - Memory + ArraySegment = 1, + Memory = 2 } - protected Func, WebSocketMessageType, bool, CancellationToken, Task> SendAsync { get; private set; } = null!; - protected Func, CancellationToken, Task> ReceiveAsync { get; private set; } = null!; + protected SendReceiveType TestType { get; private set; } - private void SelectSendReceive(SendReceiveType sendReceiveTestType) + protected Task SendAsync(WebSocket webSocket, ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) { - SendAsync = sendReceiveTestType switch + return TestType switch { - SendReceiveType.ArraySegment => static (ws, buf, opcode, eom, ct) => ws.SendAsync(buf, opcode, eom, ct), - SendReceiveType.Memory => static (ws, buf, opcode, eom, ct) => ws.SendAsync((ReadOnlyMemory)buf, opcode, eom, ct).AsTask(), - _ => throw new ArgumentException(nameof(sendReceiveTestType)) + SendReceiveType.ArraySegment => webSocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken), + SendReceiveType.Memory => SendAsMemoryAsync(webSocket, buffer, messageType, endOfMessage, cancellationToken), + _ => throw new ArgumentException(nameof(TestType)) }; - ReceiveAsync = sendReceiveTestType switch + static Task SendAsMemoryAsync(WebSocket ws, ArraySegment buf, WebSocketMessageType mt, bool eom, CancellationToken ct) + => ws.SendAsync((ReadOnlyMemory)buf, mt, eom, ct).AsTask(); + } + + protected Task ReceiveAsync(WebSocket webSocket, ArraySegment buffer, CancellationToken cancellationToken) + { + return TestType switch { - SendReceiveType.ArraySegment => static (ws, buf, ct) => ws.ReceiveAsync(buf, ct), - SendReceiveType.Memory => static async (ws, buf, ct) => - { - ValueWebSocketReceiveResult r = await ws.ReceiveAsync((Memory)buf, ct).ConfigureAwait(false); - return new WebSocketReceiveResult(r.Count, r.MessageType, r.EndOfMessage, ws.CloseStatus, ws.CloseStatusDescription); - }, - _ => throw new ArgumentException(nameof(sendReceiveTestType)) + SendReceiveType.ArraySegment => webSocket.ReceiveAsync(buffer, cancellationToken), + SendReceiveType.Memory => ReceiveAsMemoryAsync(webSocket, buffer, cancellationToken), + _ => throw new ArgumentException(nameof(TestType)) }; + + 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 Task RunSendReceive(Func sendReceiveFunc, SendReceiveType sendReceiveTestType) { - SelectSendReceive(sendReceiveTestType); + TestType = sendReceiveTestType; return sendReceiveFunc(); } - protected Task RunSendReceive(Func sendReceiveFunc, Uri uri, SendReceiveType sendReceiveTestType) - { - SelectSendReceive(sendReceiveTestType); - return sendReceiveFunc(uri); - } + => RunSendReceive(() => sendReceiveFunc(uri), sendReceiveTestType); #endregion @@ -205,7 +208,7 @@ protected async Task RunClient_SendAsync_MultipleOutstandingSendOperations_Throw { tasks[i] = SendAsync( cws, - ToUtf8("hello"), + "hello".ToUtf8(), WebSocketMessageType.Text, true, cts.Token); @@ -261,7 +264,7 @@ protected async Task RunClient_ReceiveAsync_MultipleOutstandingReceiveOperations await SendAsync( cws, - ToUtf8(EchoControlMessage.Delay5Sec), + EchoControlMessage.Delay5Sec.ToUtf8(), WebSocketMessageType.Text, true, cts.Token); @@ -321,7 +324,7 @@ protected async Task RunClient_SendAsync_SendZeroLengthPayloadAsEndOfMessage_Suc string message = "hello"; await SendAsync( cws, - ToUtf8(message), + message.ToUtf8(), WebSocketMessageType.Text, false, cts.Token); @@ -346,7 +349,7 @@ await SendAsync( Assert.Null(recvRet.CloseStatusDescription); var recvSegment = new ArraySegment(receiveSegment.Array, receiveSegment.Offset, recvRet.Count); - Assert.Equal(message, FromUtf8(recvSegment)); + Assert.Equal(message, recvSegment.Utf8ToString()); } } diff --git a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs index 061ff33ee5f189..5616c0d9fe154d 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/WebSocketHelper.cs @@ -1,13 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; using Xunit; -using Xunit.Abstractions; namespace System.Net.WebSockets.Client.Tests { @@ -19,25 +17,19 @@ public static class WebSocketHelper public static bool WebSocketsSupported { get { return s_WebSocketSupported.Value; } } public static async Task TestEcho( - Uri server, + ClientWebSocket cws, WebSocketMessageType type, - int timeOutMilliseconds, - ITestOutputHelper output, - Action configureOptions, - 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, configureOptions, invoker); - - await cws.SendAsync(ToUtf8(message), type, true, cts.Token); + await cws.SendAsync(message.ToUtf8(), type, true, cancellationToken); Assert.Equal(WebSocketState.Open, cws.State); - WebSocketReceiveResult recvRet = await cws.ReceiveAsync(receiveSegment, cts.Token); + WebSocketReceiveResult recvRet = await cws.ReceiveAsync(receiveSegment, cancellationToken); Assert.Equal(WebSocketState.Open, cws.State); Assert.Equal(message.Length, recvRet.Count); Assert.Equal(type, recvRet.MessageType); @@ -46,9 +38,9 @@ public static async Task TestEcho( Assert.Null(recvRet.CloseStatusDescription); var recvSegment = new ArraySegment(receiveSegment.Array, receiveSegment.Offset, recvRet.Count); - Assert.Equal(message, FromUtf8(recvSegment)); + Assert.Equal(message, recvSegment.Utf8ToString()); - Task taskClose = cws.CloseAsync(WebSocketCloseStatus.NormalClosure, closeMessage, cts.Token); + 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), @@ -59,83 +51,7 @@ public static async Task TestEcho( 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(); - using var cts = new CancellationTokenSource(timeOutMilliseconds); - await ConnectAsync(cws, server, configureOptions, invoker, validateState: true, cts.Token); - return cws; - }); - - public static async Task ConnectAsync( - ClientWebSocket cws, - Uri server, - Action configureOptions, - HttpMessageInvoker? invoker = null, - bool validateState = true, - CancellationToken cancellationToken = default) - { - if (PlatformDetection.IsNotBrowser && server.Scheme == "wss" && invoker == null) - { - cws.Options.RemoteCertificateValidationCallback = (_, _, _, _) => true; - } - - configureOptions(cws.Options); - - Task taskConnect = invoker == null - ? cws.ConnectAsync(server, cancellationToken) - : cws.ConnectAsync(server, invoker, cancellationToken); - - if (validateState) - { - 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; - - if (validateState) - { - Assert.Equal(WebSocketState.Open, cws.State); - } - } - - public static async Task Retry(ITestOutputHelper output, Func> func) + public static async Task Retry(Func> func) { const int MaxTries = 5; int betweenTryDelayMilliseconds = 1000; @@ -148,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); @@ -179,22 +94,14 @@ private static bool InitWebSocketSupported() } finally { - if (cws != null) - { - cws.Dispose(); - } + cws?.Dispose(); } } - public static ArraySegment ToUtf8(string text) - { - byte[] buffer = Encoding.UTF8.GetBytes(text); - return new ArraySegment(buffer); - } + public static ArraySegment ToUtf8(this string text) + => new ArraySegment(Encoding.UTF8.GetBytes(text)); - public static string FromUtf8(ArraySegment buffer) - { - return Encoding.UTF8.GetString(buffer.Array, buffer.Offset, buffer.Count); - } + 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 79fa2b4765cddc..9db52deb2a9f3f 100644 --- a/src/libraries/System.Net.WebSockets.Client/tests/wasm/BrowserTimerThrottlingTest.cs +++ b/src/libraries/System.Net.WebSockets.Client/tests/wasm/BrowserTimerThrottlingTest.cs @@ -88,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))