From c4a4680ec5e2d4d00b795bba845aff4d2187906c Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 30 Aug 2026 21:14:24 +0200 Subject: [PATCH 1/2] URL encoding and decoding: retarget net10.0, add UrlEncoder, add Uri behaviour tests - Retarget UrlEncodingAndDecoding and Tests from net7.0 to net10.0. - Bump test packages: Microsoft.NET.Test.Sdk 18.9.0, MSTest.TestAdapter and MSTest.TestFramework 4.3.3, coverlet.collector 10.0.1. Replace the obsolete DataTestMethod attribute with TestMethod (MSTEST0044). - Add the UrlEncoder.Default.Encode() example to Program.cs and a test asserting its exact output, plus a value-level encoding line showing the shape real code should use. - Add tests for Uri.EscapeDataString on a 100,000-character string (no length limit on modern .NET), on the encodeURIComponent-safe characters, and for the Uri constructor with a bare path and with a protocol-relative address. --- .../UrlEncodingAndDecoding/Tests/Tests.csproj | 10 ++-- .../Tests/UrlEncodingDecodingUnitTest.cs | 54 +++++++++++++++++-- .../UrlEncodingAndDecoding/Program.cs | 11 +++- .../UrlEncodingAndDecoding.csproj | 2 +- 4 files changed, 66 insertions(+), 11 deletions(-) diff --git a/aspnetcore-features/UrlEncodingAndDecoding/Tests/Tests.csproj b/aspnetcore-features/UrlEncodingAndDecoding/Tests/Tests.csproj index c5ecdf5955..8226e5e78e 100644 --- a/aspnetcore-features/UrlEncodingAndDecoding/Tests/Tests.csproj +++ b/aspnetcore-features/UrlEncodingAndDecoding/Tests/Tests.csproj @@ -1,7 +1,7 @@ - net7.0 + net10.0 enable enable @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/aspnetcore-features/UrlEncodingAndDecoding/Tests/UrlEncodingDecodingUnitTest.cs b/aspnetcore-features/UrlEncodingAndDecoding/Tests/UrlEncodingDecodingUnitTest.cs index 1003e561b8..06152ec685 100644 --- a/aspnetcore-features/UrlEncodingAndDecoding/Tests/UrlEncodingDecodingUnitTest.cs +++ b/aspnetcore-features/UrlEncodingAndDecoding/Tests/UrlEncodingDecodingUnitTest.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Text.Encodings.Web; using System.Web; namespace Tests @@ -22,7 +23,7 @@ public void GivenAUrl_WhenEncodingWithHttpUtility_ThenCharactersEncoded() [DataRow(EncodedUrlLowerPlus)] [DataRow(EncodedUrlUpperPercent)] [DataRow(EncodedUrlUpperPlus)] - [DataTestMethod] + [TestMethod] public void GivenAUrl_WhenDecodingWithHttpUtility_ThenCharactersDecoded(string encodedUrl) { var decoded = HttpUtility.UrlDecode(encodedUrl); @@ -41,7 +42,7 @@ public void GivenAUrl_WhenEncodingWithWebUtility_ThenCharactersEncoded() [DataRow(EncodedUrlLowerPlus)] [DataRow(EncodedUrlUpperPercent)] [DataRow(EncodedUrlUpperPlus)] - [DataTestMethod] + [TestMethod] public void GivenAUrl_WhenDecodingWithWebUtility_ThenCharactersDecoded(string encodedUrl) { var decoded = WebUtility.UrlDecode(encodedUrl); @@ -58,7 +59,7 @@ public void GivenAUrl_WhenEncodingWithUri_ThenCharactersEncoded() } [DataRow(EncodedUrlUpperPercent)] - [DataTestMethod] + [TestMethod] public void GivenAUrl_WhenDecodingWithUri_ThenCharactersDecoded(string encodedUrl) { var decoded = Uri.UnescapeDataString(encodedUrl); @@ -68,12 +69,57 @@ public void GivenAUrl_WhenDecodingWithUri_ThenCharactersDecoded(string encodedUr [DataRow(EncodedUrlLowerPlus)] [DataRow(EncodedUrlUpperPlus)] - [DataTestMethod] + [TestMethod] public void GivenAUrl_WhenDecodingWithUri_ThenCharactersNotDecoded(string encodedUrl) { var decoded = Uri.UnescapeDataString(encodedUrl); Assert.AreNotEqual(Url, decoded); //Uri.UnescapeDataString does not decode + character to space } + + [TestMethod] + public void GivenAUrl_WhenEncodingWithUrlEncoder_ThenCharactersEncoded() + { + var encoded = UrlEncoder.Default.Encode(Url); + + Assert.AreEqual(EncodedUrlUpperPercent, encoded); + } + + [TestMethod] + public void GivenAVeryLongString_WhenEscapingWithUri_ThenNoLengthLimitApplies() + { + var longValue = new string('a', 100_000) + " "; + + var encoded = Uri.EscapeDataString(longValue); + + Assert.AreEqual(100_003, encoded.Length); + } + + [TestMethod] + public void GivenTheEncodeUriComponentSafeCharacters_WhenEscapingWithUri_ThenTheyAreEscaped() + { + var encoded = Uri.EscapeDataString("!'()*~"); + + Assert.AreEqual("%21%27%28%29%2A~", encoded); + } + + [TestMethod] + public void GivenABarePath_WhenConstructingAUri_ThenUriFormatExceptionThrown() + { + Assert.ThrowsExactly(() => new Uri("/foo")); + + Assert.IsFalse(Uri.TryCreate("/foo", UriKind.Absolute, out _)); + Assert.IsTrue(Uri.TryCreate("/foo", UriKind.Relative, out _)); + } + + [TestMethod] + public void GivenAProtocolRelativeUrl_WhenConstructingAUri_ThenItParsesAsAFileUri() + { + var uri = new Uri("//example.com"); + + Assert.AreEqual("file", uri.Scheme); + Assert.AreEqual("file://example.com/", uri.AbsoluteUri); + Assert.IsTrue(Uri.TryCreate("//example.com", UriKind.Absolute, out _)); + } } } \ No newline at end of file diff --git a/aspnetcore-features/UrlEncodingAndDecoding/UrlEncodingAndDecoding/Program.cs b/aspnetcore-features/UrlEncodingAndDecoding/UrlEncodingAndDecoding/Program.cs index 8fd07966e0..87b6fb2eb9 100644 --- a/aspnetcore-features/UrlEncodingAndDecoding/UrlEncodingAndDecoding/Program.cs +++ b/aspnetcore-features/UrlEncodingAndDecoding/UrlEncodingAndDecoding/Program.cs @@ -1,4 +1,5 @@ using System.Net; +using System.Text.Encodings.Web; using System.Web; var url = @"http://example.com/resource?foo=bar with space#fragment"; @@ -12,10 +13,18 @@ var uriEncoded = Uri.EscapeDataString(url); var uriDecoded = Uri.UnescapeDataString(uriEncoded); +var urlEncoderEncoded = UrlEncoder.Default.Encode(url); + Console.WriteLine(httpUtilityEncoded); Console.WriteLine(webUtilityEncoded); Console.WriteLine(uriEncoded); +Console.WriteLine(urlEncoderEncoded); Console.WriteLine(httpUtilityDecoded); Console.WriteLine(webUtilityDecoded); -Console.WriteLine(uriDecoded); \ No newline at end of file +Console.WriteLine(uriDecoded); + +// In real code we encode a single value, not the address it goes into: +var searchUrl = $"https://example.com/search?q={Uri.EscapeDataString("bar with space")}"; + +Console.WriteLine(searchUrl); \ No newline at end of file diff --git a/aspnetcore-features/UrlEncodingAndDecoding/UrlEncodingAndDecoding/UrlEncodingAndDecoding.csproj b/aspnetcore-features/UrlEncodingAndDecoding/UrlEncodingAndDecoding/UrlEncodingAndDecoding.csproj index f02677bf64..dfb40caafc 100644 --- a/aspnetcore-features/UrlEncodingAndDecoding/UrlEncodingAndDecoding/UrlEncodingAndDecoding.csproj +++ b/aspnetcore-features/UrlEncodingAndDecoding/UrlEncodingAndDecoding/UrlEncodingAndDecoding.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net10.0 enable enable From e386eca495a0f173ca3d793ffa86fded5c8fc632 Mon Sep 17 00:00:00 2001 From: Vladimir Pecanac Date: Sun, 30 Aug 2026 21:16:49 +0200 Subject: [PATCH 2/2] Uri constructor test: state the bare-path outcome per platform new Uri("/foo") throws UriFormatException on Windows but parses as the absolute file URI file:///foo on Unix, where a bare path is a rooted local path. The test now asserts the real behaviour on each platform instead of the Windows one everywhere. new Uri("//example.com") is unchanged: it yields file://example.com/ on both. --- .../Tests/UrlEncodingDecodingUnitTest.cs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/aspnetcore-features/UrlEncodingAndDecoding/Tests/UrlEncodingDecodingUnitTest.cs b/aspnetcore-features/UrlEncodingAndDecoding/Tests/UrlEncodingDecodingUnitTest.cs index 06152ec685..ac6e1395e4 100644 --- a/aspnetcore-features/UrlEncodingAndDecoding/Tests/UrlEncodingDecodingUnitTest.cs +++ b/aspnetcore-features/UrlEncodingAndDecoding/Tests/UrlEncodingDecodingUnitTest.cs @@ -104,17 +104,33 @@ public void GivenTheEncodeUriComponentSafeCharacters_WhenEscapingWithUri_ThenThe } [TestMethod] - public void GivenABarePath_WhenConstructingAUri_ThenUriFormatExceptionThrown() + public void GivenABarePath_WhenConstructingAUri_ThenTheOutcomeIsPlatformDependent() { - Assert.ThrowsExactly(() => new Uri("/foo")); - - Assert.IsFalse(Uri.TryCreate("/foo", UriKind.Absolute, out _)); + if (OperatingSystem.IsWindows()) + { + //A bare path is not an absolute URI on Windows and not a rooted local path either. + Assert.ThrowsExactly(() => new Uri("/foo")); + Assert.IsFalse(Uri.TryCreate("/foo", UriKind.Absolute, out _)); + } + else + { + //On Unix a bare path IS a rooted local path, so it parses as an absolute file URI. + var uri = new Uri("/foo"); + + Assert.AreEqual("file", uri.Scheme); + Assert.AreEqual("file:///foo", uri.AbsoluteUri); + Assert.IsTrue(Uri.TryCreate("/foo", UriKind.Absolute, out _)); + } + + //The relative remedy works the same way on every platform. Assert.IsTrue(Uri.TryCreate("/foo", UriKind.Relative, out _)); + Assert.IsFalse(new Uri("/foo", UriKind.Relative).IsAbsoluteUri); } [TestMethod] public void GivenAProtocolRelativeUrl_WhenConstructingAUri_ThenItParsesAsAFileUri() { + //Same on Windows and on Unix: this does not throw, it becomes a file URI. var uri = new Uri("//example.com"); Assert.AreEqual("file", uri.Scheme);