From 46b7c5991f1ca03ace406d3c9fdd96ab51e0ce54 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 08:21:35 +0000 Subject: [PATCH 1/5] Initial plan From e866ec2008df7bf8f9f326ba8a5da7ca8dab77da Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 08:31:31 +0000 Subject: [PATCH 2/5] Add URL encoding support for WithPath() to handle special characters Co-authored-by: dennisdoomen <572734+dennisdoomen@users.noreply.github.com> --- Mockly.Specs/UrlEncodingTests.cs | 50 ++++++++++++++++++++++++++++++++ Mockly/RequestMockBuilder.cs | 47 +++++++++++++++++++++++++++++- 2 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 Mockly.Specs/UrlEncodingTests.cs diff --git a/Mockly.Specs/UrlEncodingTests.cs b/Mockly.Specs/UrlEncodingTests.cs new file mode 100644 index 0000000..403acb2 --- /dev/null +++ b/Mockly.Specs/UrlEncodingTests.cs @@ -0,0 +1,50 @@ +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using FluentAssertions; +using Xunit; + +namespace Mockly.Specs; + +public class UrlEncodingTests +{ + [Fact] + public async Task WithPath_should_handle_url_encoded_characters() + { + // Arrange + var mock = new HttpMock(); + var key = $"{Guid.NewGuid()}|{Guid.NewGuid()}"; + + mock.ForDelete() + .WithPath($"IncomeRelations/{key}") + .RespondsWithStatus(HttpStatusCode.OK); + + // Act + var client = mock.GetClient(); + var response = await client.DeleteAsync($"https://localhost/IncomeRelations/{key}"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + [Fact] + public async Task WithQuery_should_handle_url_encoded_characters() + { + // Arrange + var mock = new HttpMock(); + var filter = "status=active|pending"; + + mock.ForGet() + .WithPath("api/items") + .WithQuery($"filter={filter}") + .RespondsWithStatus(HttpStatusCode.OK); + + // Act + var client = mock.GetClient(); + var response = await client.GetAsync($"https://localhost/api/items?filter={filter}"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + } +} diff --git a/Mockly/RequestMockBuilder.cs b/Mockly/RequestMockBuilder.cs index 4b6f2cc..6d56d5d 100644 --- a/Mockly/RequestMockBuilder.cs +++ b/Mockly/RequestMockBuilder.cs @@ -88,7 +88,7 @@ public RequestMockBuilder ForAnyHost() /// public RequestMockBuilder WithPath(string wildcardPattern) { - pathPattern = wildcardPattern; + pathPattern = EncodePathPattern(wildcardPattern); return this; } @@ -587,4 +587,49 @@ public RequestMockResponseBuilder RespondsWith(Func % " (space) are encoded + // But () [] & = are NOT encoded in path segments + var sb = new StringBuilder(); + foreach (char c in segment) + { + if (c == '|' || c == '{' || c == '}' || c == '<' || c == '>' || + c == '%' || c == '"' || c == ' ') + { + sb.Append(Uri.HexEscape(c)); + } + else + { + sb.Append(c); + } + } + + return sb.ToString(); + } } From b4dd282155bab27ff572ec82dfa8cff22e75aa11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 08:34:26 +0000 Subject: [PATCH 3/5] Address code review feedback: improve PathEncodedChars definition and comments Co-authored-by: dennisdoomen <572734+dennisdoomen@users.noreply.github.com> --- Mockly/RequestMockBuilder.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Mockly/RequestMockBuilder.cs b/Mockly/RequestMockBuilder.cs index 6d56d5d..4d75c96 100644 --- a/Mockly/RequestMockBuilder.cs +++ b/Mockly/RequestMockBuilder.cs @@ -17,6 +17,9 @@ namespace Mockly; /// public class RequestMockBuilder { + // Characters that .NET's Uri.AbsolutePath encodes and need to be encoded in path patterns + private static readonly HashSet PathEncodedChars = new() { '|', '{', '}', '<', '>', '%', '"', ' ' }; + private readonly HttpMock mockBuilder; private readonly List customMatchers = new(); private string? pathPattern; @@ -613,14 +616,13 @@ private static string EncodePathPattern(string pattern) private static string EncodePathSegment(string segment) { - // Only encode characters that Uri.AbsolutePath actually encodes - // Based on RFC 3986 and Uri class behavior: | { } < > % " (space) are encoded - // But () [] & = are NOT encoded in path segments + // Only encode characters that .NET's Uri.AbsolutePath actually encodes. + // This is specific to .NET's Uri implementation and may differ from other URI implementations. + // Characters like () [] & = are NOT encoded by .NET's Uri in path segments. var sb = new StringBuilder(); foreach (char c in segment) { - if (c == '|' || c == '{' || c == '}' || c == '<' || c == '>' || - c == '%' || c == '"' || c == ' ') + if (PathEncodedChars.Contains(c)) { sb.Append(Uri.HexEscape(c)); } From 5efb02c288def0c188361d5288e9ba99b7ed4d20 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 30 Dec 2025 09:17:15 +0000 Subject: [PATCH 4/5] Address PR feedback: preserve ? wildcard, move tests to HttpMockSpecs, use functional names Co-authored-by: dennisdoomen <572734+dennisdoomen@users.noreply.github.com> --- Mockly.Specs/HttpMockSpecs.cs | 39 +++++++++++++++++++++++++ Mockly.Specs/UrlEncodingTests.cs | 50 -------------------------------- Mockly/RequestMockBuilder.cs | 42 ++++++++++----------------- 3 files changed, 54 insertions(+), 77 deletions(-) delete mode 100644 Mockly.Specs/UrlEncodingTests.cs diff --git a/Mockly.Specs/HttpMockSpecs.cs b/Mockly.Specs/HttpMockSpecs.cs index 4222bcd..75a8c6e 100644 --- a/Mockly.Specs/HttpMockSpecs.cs +++ b/Mockly.Specs/HttpMockSpecs.cs @@ -141,6 +141,45 @@ public async Task The_query_does_not_require_a_question_mark() response.StatusCode.Should().Be(HttpStatusCode.OK); } + [Fact] + public async Task Can_match_path_with_pipe_character() + { + // Arrange + var mock = new HttpMock(); + var key = $"{Guid.NewGuid()}|{Guid.NewGuid()}"; + + mock.ForDelete() + .WithPath($"IncomeRelations/{key}") + .RespondsWithStatus(HttpStatusCode.OK); + + // Act + var client = mock.GetClient(); + var response = await client.DeleteAsync($"https://localhost/IncomeRelations/{key}"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + } + + [Fact] + public async Task Can_match_query_with_pipe_character() + { + // Arrange + var mock = new HttpMock(); + var filter = "status=active|pending"; + + mock.ForGet() + .WithPath("api/items") + .WithQuery($"filter={filter}") + .RespondsWithStatus(HttpStatusCode.OK); + + // Act + var client = mock.GetClient(); + var response = await client.GetAsync($"https://localhost/api/items?filter={filter}"); + + // Assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + } + [Fact] public async Task Can_mock_get_request_with_json_response() { diff --git a/Mockly.Specs/UrlEncodingTests.cs b/Mockly.Specs/UrlEncodingTests.cs deleted file mode 100644 index 403acb2..0000000 --- a/Mockly.Specs/UrlEncodingTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using FluentAssertions; -using Xunit; - -namespace Mockly.Specs; - -public class UrlEncodingTests -{ - [Fact] - public async Task WithPath_should_handle_url_encoded_characters() - { - // Arrange - var mock = new HttpMock(); - var key = $"{Guid.NewGuid()}|{Guid.NewGuid()}"; - - mock.ForDelete() - .WithPath($"IncomeRelations/{key}") - .RespondsWithStatus(HttpStatusCode.OK); - - // Act - var client = mock.GetClient(); - var response = await client.DeleteAsync($"https://localhost/IncomeRelations/{key}"); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - } - - [Fact] - public async Task WithQuery_should_handle_url_encoded_characters() - { - // Arrange - var mock = new HttpMock(); - var filter = "status=active|pending"; - - mock.ForGet() - .WithPath("api/items") - .WithQuery($"filter={filter}") - .RespondsWithStatus(HttpStatusCode.OK); - - // Act - var client = mock.GetClient(); - var response = await client.GetAsync($"https://localhost/api/items?filter={filter}"); - - // Assert - response.StatusCode.Should().Be(HttpStatusCode.OK); - } -} diff --git a/Mockly/RequestMockBuilder.cs b/Mockly/RequestMockBuilder.cs index 4d75c96..b6d78a0 100644 --- a/Mockly/RequestMockBuilder.cs +++ b/Mockly/RequestMockBuilder.cs @@ -593,41 +593,29 @@ public RequestMockResponseBuilder RespondsWith(Func Date: Wed, 31 Dec 2025 07:10:32 +0000 Subject: [PATCH 5/5] Simplify approach: decode path before matching instead of encoding pattern Co-authored-by: dennisdoomen <572734+dennisdoomen@users.noreply.github.com> --- Mockly/RequestMock.cs | 2 +- Mockly/RequestMockBuilder.cs | 37 +----------------------------------- 2 files changed, 2 insertions(+), 37 deletions(-) diff --git a/Mockly/RequestMock.cs b/Mockly/RequestMock.cs index e46880b..d30be1a 100644 --- a/Mockly/RequestMock.cs +++ b/Mockly/RequestMock.cs @@ -85,7 +85,7 @@ public async Task Matches(RequestInfo request) // Check path pattern if specified if (PathPattern != null) { - var path = request.Uri?.AbsolutePath ?? string.Empty; + var path = WebUtility.UrlDecode(request.Uri?.AbsolutePath ?? string.Empty); if (!MatchesPattern(path.TrimStart('/'), PathPattern.TrimStart('/'))) { return false; diff --git a/Mockly/RequestMockBuilder.cs b/Mockly/RequestMockBuilder.cs index b6d78a0..4b6f2cc 100644 --- a/Mockly/RequestMockBuilder.cs +++ b/Mockly/RequestMockBuilder.cs @@ -17,9 +17,6 @@ namespace Mockly; /// public class RequestMockBuilder { - // Characters that .NET's Uri.AbsolutePath encodes and need to be encoded in path patterns - private static readonly HashSet PathEncodedChars = new() { '|', '{', '}', '<', '>', '%', '"', ' ' }; - private readonly HttpMock mockBuilder; private readonly List customMatchers = new(); private string? pathPattern; @@ -91,7 +88,7 @@ public RequestMockBuilder ForAnyHost() /// public RequestMockBuilder WithPath(string wildcardPattern) { - pathPattern = EncodePathPattern(wildcardPattern); + pathPattern = wildcardPattern; return this; } @@ -590,36 +587,4 @@ public RequestMockResponseBuilder RespondsWith(Func