From 6c171386212088e790266f027e0d0f9e31ba7392 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 14 Jul 2026 00:36:35 +0200 Subject: [PATCH 1/3] [tests] Use local servers for remaining networking tests PR #12022 migrated most on-device networking tests off live external URLs to the loopback `LocalHttpServer`/`LocalHttpsServer`, and #12058 handles `SslTest.HttpsShouldWork`. This migrates the remaining tests that still reached out to the public internet, which made them flaky when those endpoints were slow or unavailable: - `SslTest.SslWithinTasksShouldWork` (dotnet.microsoft.com) - `ProxyTest.QuoteInvalidQuoteUrlsShouldWork` (msftconnecttest.com) - keeps the `?query&foo|bar` unescaped-pipe scenario against `/ok`. - `AndroidMessageHandlerTests.ServerCertificateCustomValidationCallback_ApproveRequest` and `_RejectRequest` (www.microsoft.com) - `AndroidMessageHandlerTests.AndroidMessageHandlerSendsClientCertificate` (corefx-net-tls.azurewebsites.net) - `AndroidMessageHandlerIntegrationTests.GetString_Many` (google.com) - `HttpClientHandlerTestBase.Disposed` (google.com) - `WebSocketTests.TestSocketConnection` (echo.websocket.org) - was `[Ignore]`d because the echo server no longer exists; re-enabled against a local server. To support these, `LocalTestServers.cs` gains: - Opt-in client-certificate (mutual TLS) support on `LocalHttpsServer` plus an `/echo-client-certificate` endpoint that returns the client certificate the server received. - A new loopback `LocalWebSocketServer` that performs the WebSocket upgrade handshake and echoes messages back. Also cleaned up cosmetic external hostnames in `CancelRequestViaProxy` (request never connects; goes through a dead proxy) and the `DisallowAutoRedirect` redirect target (never followed). The `http://10.255.255.1` cancellation/timeout tests intentionally use an unroutable address and are left unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 289a099a-278f-432b-b2b4-aac036e6e2dc --- .../System.Net/ProxyTest.cs | 7 +- .../Mono.Android-Tests/System.Net/SslTest.cs | 8 +- .../System.Net/WebSocketTests.cs | 7 +- .../AndroidHandlerTestBase.cs | 3 +- .../AndroidMessageHandlerIntegrationTests.cs | 10 +- .../AndroidMessageHandlerTests.cs | 21 +- .../Xamarin.Android.Net/LocalTestServers.cs | 200 +++++++++++++++++- 7 files changed, 228 insertions(+), 28 deletions(-) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/ProxyTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/ProxyTest.cs index 10fb4158ed8..a109d60bb04 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/ProxyTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/ProxyTest.cs @@ -4,6 +4,8 @@ using NUnit.Framework; +using Xamarin.Android.NetTests; + namespace System.NetTests { [TestFixture, Category ("InetAccess")] @@ -13,8 +15,9 @@ public class ProxyTest { [Test] public void QuoteInvalidQuoteUrlsShouldWork () { + using var server = LocalHttpServer.Start (); try { - string url = "http://www.msftconnecttest.com/connecttest.txt?query&foo|bar"; + string url = $"{server.Url}ok?query&foo|bar"; var request = (HttpWebRequest) WebRequest.Create (url); request.Method = "GET"; var response = (HttpWebResponse) request.GetResponse (); @@ -34,6 +37,8 @@ public void QuoteInvalidQuoteUrlsShouldWork () ex.Status == WebExceptionStatus.Timeout) { Assert.Ignore ($"Ignoring network failure: {ex.Message}"); } + + server.AssertNoUnhandledExceptions (); } } } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/SslTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/SslTest.cs index 5a9e23f1c14..3e54aa21819 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/SslTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/SslTest.cs @@ -8,6 +8,8 @@ using NUnit.Framework; +using Xamarin.Android.NetTests; + namespace System.NetTests { // TODO: https://github.com/dotnet/android/issues/10069 [TestFixture, Category ("InetAccess"), Category ("SSL")] @@ -29,6 +31,7 @@ bool ShouldIgnoreException (WebException wex) [Test] public void SslWithinTasksShouldWork () { + using var server = LocalHttpsServer.Start (); var cb = ServicePointManager.ServerCertificateValidationCallback; ServicePointManager.ServerCertificateValidationCallback = (s, cert, chain, policy) => { Console.WriteLine ("# ServerCertificateValidationCallback"); @@ -39,9 +42,7 @@ public void SslWithinTasksShouldWork () Exception exception = null; var thread = new Thread (() => { - string url = "https://dotnet.microsoft.com/"; - - var downloadTask = new WebClient ().DownloadDataTaskAsync (url); + var downloadTask = new WebClient ().DownloadDataTaskAsync (server.OkUri); var completeTask = downloadTask.ContinueWith (t => { Console.WriteLine ("# DownloadDataTaskAsync complete; status={0}; exception={1}", t.Status, t.Exception); status = t.Status; @@ -66,6 +67,7 @@ public void SslWithinTasksShouldWork () throw exception; Assert.AreEqual (TaskStatus.RanToCompletion, status); + server.AssertNoUnhandledExceptions (); } [Test] diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/WebSocketTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/WebSocketTests.cs index 7dd880bdf9a..e72f5ec91a7 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/WebSocketTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/WebSocketTests.cs @@ -4,20 +4,23 @@ using System.Threading; using System.Threading.Tasks; +using Xamarin.Android.NetTests; + namespace System.NetTests { [TestFixture] public class WebSocketTests { [Test, Category ("InetAccess")] - [Ignore ("echo.websocket.org is not available anymore")] public void TestSocketConnection() { string testMessage = "This is a test!"; var messageBytes = CustomWebSocket.GetBytes (testMessage); CustomWebSocket.BytesSize = messageBytes.Length; - var result = CustomWebSocket.Connect ("ws://echo.websocket.org", messageBytes).Result; + using var server = LocalWebSocketServer.Start (); + var result = CustomWebSocket.Connect (server.Url, messageBytes).Result; Assert.AreEqual (result, testMessage, $"Socket test failed. Expected: {testMessage}, Received: {result}"); + server.AssertNoUnhandledExceptions (); } } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidHandlerTestBase.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidHandlerTestBase.cs index 4b1878643e9..154a1fec207 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidHandlerTestBase.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidHandlerTestBase.cs @@ -73,11 +73,12 @@ public bool IsBypassed (Uri host) [Test] public void Disposed () { + using var server = LocalHttpServer.Start (); var h = CreateHandler (); h.Dispose (); var c = new HttpClient (h); try { - var t = ConnectIgnoreFailure (() => c.GetAsync ("http://google.com"), out bool connectionFailed); + var t = ConnectIgnoreFailure (() => c.GetAsync (server.OkUri), out bool connectionFailed); if (connectionFailed) return; diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerIntegrationTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerIntegrationTests.cs index 8daa6bf191b..b82d486c5a2 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerIntegrationTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerIntegrationTests.cs @@ -147,7 +147,7 @@ public void CancelRequestViaProxy () handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; var httpClient = new HttpClient (handler) { - BaseAddress = new Uri ("https://google.com"), + BaseAddress = new Uri ("https://localhost/"), Timeout = TimeSpan.FromMilliseconds (1) }; @@ -273,10 +273,12 @@ public void Send_Invalid () [Test] public void GetString_Many () { + using var server = LocalHttpServer.Start (); var client = new HttpClient (new Xamarin.Android.Net.AndroidMessageHandler ()); - var t1 = client.GetStringAsync ("https://google.com"); - var t2 = client.GetStringAsync ("https://google.com"); + var t1 = client.GetStringAsync (server.OkUri); + var t2 = client.GetStringAsync (server.OkUri); Assert.IsTrue (Task.WaitAll (new [] { t1, t2 }, WaitTimeout)); + server.AssertNoUnhandledExceptions (); } [Test] @@ -285,7 +287,7 @@ public void DisallowAutoRedirect () var listener = CreateListener (l => { using (var response = l.Response) { - response.Redirect("http://xamarin.com/"); + response.Redirect("http://localhost/"); } }); diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs index 1b1e7303705..84fc6749e13 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs @@ -111,16 +111,15 @@ public async Task DoesNotDisposeContentStream() public async Task ServerCertificateCustomValidationCallback_ApproveRequest () { bool callbackHasBeenCalled = false; + using var server = LocalHttpsServer.Start (); var handler = new AndroidMessageHandler { ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => { Assert.NotNull (request, "request"); - Assert.AreEqual ("www.microsoft.com", request.RequestUri.Host); + Assert.AreEqual ("localhost", request.RequestUri.Host); Assert.NotNull (cert, "cert"); - Assert.True (cert!.Subject.Contains ("www.microsoft.com"), $"Unexpected certificate subject {cert!.Subject}"); - Assert.True (cert!.Issuer.Contains ("Microsoft"), $"Unexpected certificate issuer {cert!.Issuer}"); + Assert.True (cert.Subject.Contains ("localhost"), $"Unexpected certificate subject {cert.Subject}"); Assert.NotNull (chain, "chain"); - Assert.AreEqual (SslPolicyErrors.None, errors); callbackHasBeenCalled = true; return true; @@ -128,15 +127,17 @@ public async Task ServerCertificateCustomValidationCallback_ApproveRequest () }; var client = new HttpClient (handler); - await client.GetStringAsync ("https://www.microsoft.com/"); + Assert.AreEqual ("OK", await client.GetStringAsync (server.OkUri)); Assert.IsTrue (callbackHasBeenCalled, "custom validation callback hasn't been called"); + server.AssertNoUnhandledExceptions (); } [Test] public async Task ServerCertificateCustomValidationCallback_RejectRequest () { bool callbackHasBeenCalled = false; + using var server = LocalHttpsServer.Start (); var handler = new AndroidMessageHandler { ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => { @@ -146,7 +147,7 @@ public async Task ServerCertificateCustomValidationCallback_RejectRequest () }; var client = new HttpClient (handler); - await AssertRejectsRemoteCertificate (() => client.GetStringAsync ("https://www.microsoft.com/")); + await AssertRejectsRemoteCertificate (() => client.GetStringAsync (server.OkUri)); Assert.IsTrue (callbackHasBeenCalled, "custom validation callback hasn't been called"); } @@ -259,19 +260,23 @@ public async Task AndroidMessageHandlerFollows308PermanentRedirect () public async Task AndroidMessageHandlerSendsClientCertificate ([Values(true, false)] bool setClientCertificateOptionsExplicitly) { using X509Certificate2 certificate = BuildClientCertificate (); + using var server = LocalHttpsServer.Start (requestClientCertificate: true); - using var handler = new AndroidMessageHandler (); + using var handler = new AndroidMessageHandler { + ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => true, + }; if (setClientCertificateOptionsExplicitly) { handler.ClientCertificateOptions = ClientCertificateOption.Manual; } handler.ClientCertificates.Add (certificate); using var client = new HttpClient (handler); - var response = await client.GetAsync ("https://corefx-net-tls.azurewebsites.net/EchoClientCertificate.ashx"); + var response = await client.GetAsync (server.GetUri ("echo-client-certificate")); var content = await response.EnsureSuccessStatusCode ().Content.ReadAsStringAsync (); X509Certificate2 certificate2 = new X509Certificate2 (global::System.Convert.FromBase64String (content)); Assert.AreEqual (certificate.Thumbprint, certificate2.Thumbprint); + server.AssertNoUnhandledExceptions (); } [Test] diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs index bd29a60ef4e..066bfb855e0 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs @@ -5,9 +5,11 @@ using System.Net; using System.Net.Sockets; using System.Text; +using System.Threading; using System.Threading.Tasks; #if !NETSTANDARD2_0 using System.Net.Security; +using System.Net.WebSockets; using System.Security.Authentication; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; @@ -238,7 +240,7 @@ static int GetContentLength (string[] headers) return 0; } - static Task HandleRequest (Stream stream, LocalHttpRequest request) + protected virtual Task HandleRequest (Stream stream, LocalHttpRequest request) { switch (request.Path) { #if !NETSTANDARD2_0 @@ -350,7 +352,7 @@ static string GetReasonPhrase (HttpStatusCode statusCode) } } - sealed class LocalHttpRequest + protected sealed class LocalHttpRequest { public LocalHttpRequest (string method, string target, string body) { @@ -402,12 +404,14 @@ sealed class LocalHttpsServer : LocalHttpServer { readonly RSA certificateKey; readonly X509Certificate2 certificate; + readonly bool requestClientCertificate; - LocalHttpsServer (string certificateHost) + LocalHttpsServer (string certificateHost, bool requestClientCertificate) : base ("Local HTTPS server") { certificateKey = RSA.Create (keySizeInBits: 2048); certificate = CreateCertificate (certificateKey, certificateHost); + this.requestClientCertificate = requestClientCertificate; } public byte [] CertificateData { @@ -422,14 +426,14 @@ protected override string Host { get { return "localhost"; } } - public static LocalHttpsServer Start () + public static LocalHttpsServer Start (bool requestClientCertificate = false) { - return Start ("localhost"); + return Start ("localhost", requestClientCertificate); } - public static LocalHttpsServer Start (string certificateHost) + public static LocalHttpsServer Start (string certificateHost, bool requestClientCertificate = false) { - var server = new LocalHttpsServer (certificateHost); + var server = new LocalHttpsServer (certificateHost, requestClientCertificate); server.StartListening (); return server; } @@ -443,11 +447,30 @@ public override void Dispose () protected override async Task GetRequestStream (TcpClient client) { - var sslStream = new SslStream (client.GetStream (), leaveInnerStreamOpen: false); - await sslStream.AuthenticateAsServerAsync (certificate, clientCertificateRequired: false, enabledSslProtocols: SslProtocols.None, checkCertificateRevocation: false).ConfigureAwait (false); + var sslStream = new SslStream (client.GetStream (), leaveInnerStreamOpen: false, userCertificateValidationCallback: (sender, clientCertificate, chain, sslPolicyErrors) => true); + await sslStream.AuthenticateAsServerAsync (certificate, clientCertificateRequired: requestClientCertificate, enabledSslProtocols: SslProtocols.None, checkCertificateRevocation: false).ConfigureAwait (false); return sslStream; } + protected override Task HandleRequest (Stream stream, LocalHttpRequest request) + { + if (request.Path == "/echo-client-certificate") { + return WriteClientCertificateAsync (stream); + } + + return base.HandleRequest (stream, request); + } + + static Task WriteClientCertificateAsync (Stream stream) + { + string clientCertificateData = ""; + if (stream is SslStream sslStream && sslStream.RemoteCertificate != null) { + clientCertificateData = Convert.ToBase64String (sslStream.RemoteCertificate.Export (X509ContentType.Cert)); + } + + return WriteStringAsync (stream, clientCertificateData, "text/plain"); + } + static X509Certificate2 CreateCertificate (RSA key, string certificateHost) { DateTimeOffset start = DateTimeOffset.UtcNow.AddDays (-30); @@ -469,5 +492,164 @@ static X509Certificate2 CreateCertificate (RSA key, string certificateHost) return request.CreateSelfSigned (start, end); } } + + sealed class LocalWebSocketServer : LocalTestServer + { + const string WebSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + readonly TcpListener listener; + Task acceptLoop = Task.CompletedTask; + bool disposed; + + LocalWebSocketServer () + : base ("Local WebSocket server") + { + listener = new TcpListener (IPAddress.Loopback, 0); + } + + public int Port { get; private set; } + + public Uri Uri { + get { return new Uri ($"ws://{LoopbackHost}:{Port}/"); } + } + + public string Url { + get { return Uri.ToString (); } + } + + public static LocalWebSocketServer Start () + { + var server = new LocalWebSocketServer (); + server.StartListening (); + return server; + } + + public override void Dispose () + { + disposed = true; + listener.Stop (); + WaitForShutdown (acceptLoop, inner => inner is ObjectDisposedException || inner is SocketException); + } + + void StartListening () + { + listener.Start (); + Port = ((IPEndPoint) listener.LocalEndpoint).Port; + acceptLoop = Task.Run (AcceptLoop); + } + + async Task AcceptLoop () + { + while (!disposed) { + TcpClient client; + try { + client = await listener.AcceptTcpClientAsync ().ConfigureAwait (false); + } catch (ObjectDisposedException) { + return; + } catch (SocketException) when (disposed) { + return; + } + + _ = Task.Run (() => HandleClient (client)); + } + } + + async Task HandleClient (TcpClient client) + { + using (client) { + bool handshakeCompleted = false; + try { + Stream stream = client.GetStream (); + string key = await ReadWebSocketKeyAsync (stream).ConfigureAwait (false); + if (key == null) { + return; + } + + await WriteHandshakeResponseAsync (stream, key).ConfigureAwait (false); + handshakeCompleted = true; + + using var webSocket = WebSocket.CreateFromStream (stream, isServer: true, subProtocol: null, keepAliveInterval: TimeSpan.FromSeconds (30)); + await EchoLoop (webSocket).ConfigureAwait (false); + } catch (Exception ex) { + if (!(handshakeCompleted && (ex is IOException || ex is ObjectDisposedException || ex is WebSocketException))) { + AddHandlerException (ex); + } + } + } + } + + static async Task ReadWebSocketKeyAsync (Stream stream) + { + byte[] endOfHeaders = Encoding.ASCII.GetBytes ("\r\n\r\n"); + byte[] buffer = new byte [1]; + int matched = 0; + + using var headersStream = new MemoryStream (); + while (headersStream.Length < 64 * 1024) { + int read = await stream.ReadAsync (buffer, 0, buffer.Length).ConfigureAwait (false); + if (read == 0) { + break; + } + + headersStream.WriteByte (buffer [0]); + if (buffer [0] == endOfHeaders [matched]) { + matched++; + if (matched == endOfHeaders.Length) { + break; + } + } else { + matched = buffer [0] == endOfHeaders [0] ? 1 : 0; + } + } + + string headers = Encoding.ASCII.GetString (headersStream.ToArray ()); + foreach (string line in headers.Split (new [] { "\r\n" }, StringSplitOptions.None)) { + if (line.StartsWith ("Sec-WebSocket-Key:", StringComparison.OrdinalIgnoreCase)) { + return line.Substring ("Sec-WebSocket-Key:".Length).Trim (); + } + } + + return null; + } + + static Task WriteHandshakeResponseAsync (Stream stream, string key) + { + string accept; + using (var sha1 = SHA1.Create ()) { + byte[] hash = sha1.ComputeHash (Encoding.ASCII.GetBytes (key + WebSocketGuid)); + accept = Convert.ToBase64String (hash); + } + + var response = new StringBuilder (); + response.Append ("HTTP/1.1 101 Switching Protocols\r\n"); + response.Append ("Upgrade: websocket\r\n"); + response.Append ("Connection: Upgrade\r\n"); + response.Append ("Sec-WebSocket-Accept: ").Append (accept).Append ("\r\n\r\n"); + + byte[] bytes = Encoding.ASCII.GetBytes (response.ToString ()); + return stream.WriteAsync (bytes, 0, bytes.Length); + } + + static async Task EchoLoop (WebSocket webSocket) + { + byte[] buffer = new byte [4096]; + while (webSocket.State == WebSocketState.Open) { + WebSocketReceiveResult result; + try { + result = await webSocket.ReceiveAsync (new ArraySegment (buffer), CancellationToken.None).ConfigureAwait (false); + } catch (Exception ex) when (ex is WebSocketException || ex is IOException || ex is ObjectDisposedException) { + // The client closed the connection without a WebSocket close handshake. + return; + } + + if (result.MessageType == WebSocketMessageType.Close) { + await webSocket.CloseAsync (WebSocketCloseStatus.NormalClosure, "", CancellationToken.None).ConfigureAwait (false); + return; + } + + await webSocket.SendAsync (new ArraySegment (buffer, 0, result.Count), result.MessageType, result.EndOfMessage, CancellationToken.None).ConfigureAwait (false); + } + } + } #endif } From 44b43f81a285639512a8e0d336980382d01a89a3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 14 Jul 2026 11:10:41 +0200 Subject: [PATCH 2/3] [tests] Suppress CA5350 for RFC 6455 WebSocket handshake SHA-1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The local WebSocket test server computes the Sec-WebSocket-Accept value using SHA-1, as mandated by the WebSocket handshake (RFC 6455 §1.3). This is a protocol requirement, not a security-sensitive choice, but CA5350 (treated as an error in the test build) flagged it and broke the Mono.Android.NET-Tests APK build. Suppress CA5350 around the SHA-1 usage with a justification. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 528dc903-4f7c-4a75-b74b-da0fbfe23f01 --- .../Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs index 066bfb855e0..55d7b67760a 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs @@ -615,10 +615,12 @@ static async Task ReadWebSocketKeyAsync (Stream stream) static Task WriteHandshakeResponseAsync (Stream stream, string key) { string accept; +#pragma warning disable CA5350 // SHA-1 is mandated by the WebSocket handshake (RFC 6455 §1.3) using (var sha1 = SHA1.Create ()) { byte[] hash = sha1.ComputeHash (Encoding.ASCII.GetBytes (key + WebSocketGuid)); accept = Convert.ToBase64String (hash); } +#pragma warning restore CA5350 var response = new StringBuilder (); response.Append ("HTTP/1.1 101 Switching Protocols\r\n"); From f620ce01e0d9b84ae73ab5b4b94ff6fc8ad7fcb2 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 14 Jul 2026 11:49:17 +0200 Subject: [PATCH 3/3] [tests] Address code review: remove loopback ignore paths, rename flag Addresses PR review feedback now that these tests target loopback servers: - ProxyTest.QuoteInvalidQuoteUrlsShouldWork: drop the ConnectFailure/ NameResolutionFailure/Timeout Assert.Ignore path; loopback failures are real regressions and should fail the test. - SslTest.SslWithinTasksShouldWork: rethrow the WebException instead of ignoring 'network failure' (loopback is always reachable). - HttpClientHandlerTestBase.Disposed: call GetAsync directly instead of via ConnectIgnoreFailure so an unexpected exception type isn't masked. - LocalHttpsServer: rename requestClientCertificate -> clientCertificateRequired to match AuthenticateAsServerAsync's clientCertificateRequired argument. Not applied: adding AssertNoUnhandledExceptions() to ServerCertificateCustomValidationCallback_RejectRequest. When the client rejects the server certificate the TLS handshake is aborted, so the server's AuthenticateAsServerAsync throws before handlerCompleted becomes true and the exception is recorded; asserting no handler exceptions would fail that test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 528dc903-4f7c-4a75-b74b-da0fbfe23f01 --- .../System.Net/ProxyTest.cs | 31 +++++++------------ .../Mono.Android-Tests/System.Net/SslTest.cs | 4 --- .../AndroidHandlerTestBase.cs | 5 +-- .../AndroidMessageHandlerTests.cs | 2 +- .../Xamarin.Android.Net/LocalTestServers.cs | 16 +++++----- 5 files changed, 22 insertions(+), 36 deletions(-) diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/ProxyTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/ProxyTest.cs index a109d60bb04..ea304c118e4 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/ProxyTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/ProxyTest.cs @@ -16,27 +16,20 @@ public class ProxyTest { public void QuoteInvalidQuoteUrlsShouldWork () { using var server = LocalHttpServer.Start (); - try { - string url = $"{server.Url}ok?query&foo|bar"; - var request = (HttpWebRequest) WebRequest.Create (url); - request.Method = "GET"; - var response = (HttpWebResponse) request.GetResponse (); - int len = 0; - using (var _r = new StreamReader (response.GetResponseStream ())) { - char[] buf = new char [4096]; - int n; - while ((n = _r.Read (buf, 0, buf.Length)) > 0) { - /* ignore; we just want to make sure we can read */ - len += n; - } + string url = $"{server.Url}ok?query&foo|bar"; + var request = (HttpWebRequest) WebRequest.Create (url); + request.Method = "GET"; + var response = (HttpWebResponse) request.GetResponse (); + int len = 0; + using (var _r = new StreamReader (response.GetResponseStream ())) { + char[] buf = new char [4096]; + int n; + while ((n = _r.Read (buf, 0, buf.Length)) > 0) { + /* ignore; we just want to make sure we can read */ + len += n; } - Assert.IsTrue (len > 0); - } catch (WebException ex) when ( - ex.Status == WebExceptionStatus.ConnectFailure || - ex.Status == WebExceptionStatus.NameResolutionFailure || - ex.Status == WebExceptionStatus.Timeout) { - Assert.Ignore ($"Ignoring network failure: {ex.Message}"); } + Assert.IsTrue (len > 0); server.AssertNoUnhandledExceptions (); } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/SslTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/SslTest.cs index 3e54aa21819..8ca5f06222d 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/SslTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/System.Net/SslTest.cs @@ -56,10 +56,6 @@ public void SslWithinTasksShouldWork () ServicePointManager.ServerCertificateValidationCallback = cb; var wex = (exception as AggregateException)?.InnerException as WebException; if (wex != null) { - if (ShouldIgnoreException (wex)) { - Assert.Ignore ($"Ignoring network failure: {wex}"); - return; - } throw wex; } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidHandlerTestBase.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidHandlerTestBase.cs index 154a1fec207..c4c65ec46e1 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidHandlerTestBase.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidHandlerTestBase.cs @@ -78,10 +78,7 @@ public void Disposed () h.Dispose (); var c = new HttpClient (h); try { - var t = ConnectIgnoreFailure (() => c.GetAsync (server.OkUri), out bool connectionFailed); - if (connectionFailed) - return; - + var t = c.GetAsync (server.OkUri); t.Wait (); Assert.Fail ("#1"); } catch (AggregateException e) { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs index 84fc6749e13..f1ffd3520b4 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/AndroidMessageHandlerTests.cs @@ -260,7 +260,7 @@ public async Task AndroidMessageHandlerFollows308PermanentRedirect () public async Task AndroidMessageHandlerSendsClientCertificate ([Values(true, false)] bool setClientCertificateOptionsExplicitly) { using X509Certificate2 certificate = BuildClientCertificate (); - using var server = LocalHttpsServer.Start (requestClientCertificate: true); + using var server = LocalHttpsServer.Start (clientCertificateRequired: true); using var handler = new AndroidMessageHandler { ServerCertificateCustomValidationCallback = (request, cert, chain, errors) => true, diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs index 55d7b67760a..90d3cc775b6 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.Net/LocalTestServers.cs @@ -404,14 +404,14 @@ sealed class LocalHttpsServer : LocalHttpServer { readonly RSA certificateKey; readonly X509Certificate2 certificate; - readonly bool requestClientCertificate; + readonly bool clientCertificateRequired; - LocalHttpsServer (string certificateHost, bool requestClientCertificate) + LocalHttpsServer (string certificateHost, bool clientCertificateRequired) : base ("Local HTTPS server") { certificateKey = RSA.Create (keySizeInBits: 2048); certificate = CreateCertificate (certificateKey, certificateHost); - this.requestClientCertificate = requestClientCertificate; + this.clientCertificateRequired = clientCertificateRequired; } public byte [] CertificateData { @@ -426,14 +426,14 @@ protected override string Host { get { return "localhost"; } } - public static LocalHttpsServer Start (bool requestClientCertificate = false) + public static LocalHttpsServer Start (bool clientCertificateRequired = false) { - return Start ("localhost", requestClientCertificate); + return Start ("localhost", clientCertificateRequired); } - public static LocalHttpsServer Start (string certificateHost, bool requestClientCertificate = false) + public static LocalHttpsServer Start (string certificateHost, bool clientCertificateRequired = false) { - var server = new LocalHttpsServer (certificateHost, requestClientCertificate); + var server = new LocalHttpsServer (certificateHost, clientCertificateRequired); server.StartListening (); return server; } @@ -448,7 +448,7 @@ public override void Dispose () protected override async Task GetRequestStream (TcpClient client) { var sslStream = new SslStream (client.GetStream (), leaveInnerStreamOpen: false, userCertificateValidationCallback: (sender, clientCertificate, chain, sslPolicyErrors) => true); - await sslStream.AuthenticateAsServerAsync (certificate, clientCertificateRequired: requestClientCertificate, enabledSslProtocols: SslProtocols.None, checkCertificateRevocation: false).ConfigureAwait (false); + await sslStream.AuthenticateAsServerAsync (certificate, clientCertificateRequired: clientCertificateRequired, enabledSslProtocols: SslProtocols.None, checkCertificateRevocation: false).ConfigureAwait (false); return sslStream; }