From 97c45a11a615e0fe3bd3aba8986d932730cfed7a Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 10 May 2026 17:27:11 +0100 Subject: [PATCH 1/2] fix(mocks-http): auto-prepend `/` to partial endpoint paths `Mock.HttpClient(...).Handler.OnPost("my_endpoint")` previously failed to match because the request URI's `PathAndQuery` is `/my_endpoint`. Normalize in `RequestMatcher.Path()` and `PathStartsWith()` so partial and rooted forms are equivalent. Absolute URLs (containing `://`) and regex patterns are left untouched. Closes #5838 --- .../MockHttpHandlerTests.cs | 46 +++++++++++++++++++ TUnit.Mocks.Http/RequestMatcher.cs | 12 ++++- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/TUnit.Mocks.Http.Tests/MockHttpHandlerTests.cs b/TUnit.Mocks.Http.Tests/MockHttpHandlerTests.cs index 6872794ed30..6376057178b 100644 --- a/TUnit.Mocks.Http.Tests/MockHttpHandlerTests.cs +++ b/TUnit.Mocks.Http.Tests/MockHttpHandlerTests.cs @@ -559,4 +559,50 @@ public async Task FactoryDelegateSeesOriginalContentType() await Assert.That(capturedContentType).IsEqualTo("application/json"); } + + [Test] + public async Task MatchesByPath_AutoPrependsSlashWhenMissing() + { + using var client = Mock.HttpClient("https://my.domain/"); + client.Handler.OnPost("my_endpoint").Respond(HttpStatusCode.Accepted); + + var response = await client.PostAsync("my_endpoint", null); + + await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.Accepted); + } + + [Test] + public async Task MatchesByPathPrefix_AutoPrependsSlashWhenMissing() + { + using var client = Mock.HttpClient("http://localhost"); + client.Handler.OnRequest(r => r.Method(HttpMethod.Get).PathStartsWith("api/v2")) + .Respond(HttpStatusCode.OK); + + var response = await client.GetAsync("/api/v2/anything/here"); + + await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK); + } + + [Test] + public async Task MatchesByPath_KeepsAbsoluteUrlAsIs() + { + using var client = Mock.HttpClient(); + client.Handler.OnGet("https://my.domain/foo").Respond(HttpStatusCode.OK); + + var response = await client.GetAsync("https://my.domain/foo"); + + await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK); + } + + [Test] + public async Task MatchesByPath_RegexNotNormalized() + { + using var client = Mock.HttpClient("http://localhost"); + client.Handler.OnRequest(r => r.Method(HttpMethod.Get).PathMatches(@"^/api/users/\d+$")) + .Respond(HttpStatusCode.OK); + + var response = await client.GetAsync("/api/users/42"); + + await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK); + } } diff --git a/TUnit.Mocks.Http/RequestMatcher.cs b/TUnit.Mocks.Http/RequestMatcher.cs index 88c4707a317..880139ad8a4 100644 --- a/TUnit.Mocks.Http/RequestMatcher.cs +++ b/TUnit.Mocks.Http/RequestMatcher.cs @@ -26,17 +26,25 @@ public RequestMatcher Method(HttpMethod method) /// Match requests to the exact path. public RequestMatcher Path(string path) { - _exactPath = path; + _exactPath = NormalizePath(path); return this; } /// Match requests whose path starts with the specified prefix. public RequestMatcher PathStartsWith(string prefix) { - _pathPrefix = prefix; + _pathPrefix = NormalizePath(prefix); return this; } + private static string NormalizePath(string value) + { + if (string.IsNullOrEmpty(value)) return value; + if (value[0] == '/') return value; + if (value.Contains("://", StringComparison.Ordinal)) return value; + return "/" + value; + } + /// Match requests whose path matches the regex pattern. public RequestMatcher PathMatches(string pattern) { From d98a3392e124c3cec9e53cd3416cc7c2c4e958ab Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 10 May 2026 17:29:56 +0100 Subject: [PATCH 2/2] refactor: use Uri.TryCreate and merge null-or-rooted guard in NormalizePath --- TUnit.Mocks.Http/RequestMatcher.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/TUnit.Mocks.Http/RequestMatcher.cs b/TUnit.Mocks.Http/RequestMatcher.cs index 880139ad8a4..5a9d48d3ede 100644 --- a/TUnit.Mocks.Http/RequestMatcher.cs +++ b/TUnit.Mocks.Http/RequestMatcher.cs @@ -39,9 +39,8 @@ public RequestMatcher PathStartsWith(string prefix) private static string NormalizePath(string value) { - if (string.IsNullOrEmpty(value)) return value; - if (value[0] == '/') return value; - if (value.Contains("://", StringComparison.Ordinal)) return value; + if (string.IsNullOrEmpty(value) || value[0] == '/') return value; + if (Uri.TryCreate(value, UriKind.Absolute, out _)) return value; return "/" + value; }