diff --git a/DevProxy.Integration.Tests/BehaviorPluginsIntegrationTests.cs b/DevProxy.Integration.Tests/BehaviorPluginsIntegrationTests.cs index aa39cf27..f16a7fcd 100644 --- a/DevProxy.Integration.Tests/BehaviorPluginsIntegrationTests.cs +++ b/DevProxy.Integration.Tests/BehaviorPluginsIntegrationTests.cs @@ -66,6 +66,42 @@ public async Task GenericRandomError_Rate100_OverridesOriginWithConfiguredError( Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); } + [Theory] + [InlineData("/Get")] + [InlineData("/Ge*")] + public async Task GenericRandomError_MatchesUrlCaseInsensitively(string errorPath) + { + await using var origin = await FakeOrigin.StartAsync(); + var urls = KestrelProxyHarness.BuildUrlsToWatch(origin.Host); + + var config = PluginConfig.FromJson($$""" + { + "rate": 100, + "errors": [ + { + "request": { "url": "http://{{origin.Host}}{{errorPath}}" }, + "responses": [ { "statusCode": 503 } ] + } + ] + } + """); + var plugin = new GenericRandomErrorPlugin( + SharedHttpClient, + NullLogger.Instance, + urls, + ProxyConfig, + config); + + await using var proxy = await KestrelProxyHarness.StartAsync( + origin.Host, [plugin]); + using var client = proxy.CreateHttpClient(); + + // urlsToWatch matching ignores case, so error URL matching must too. + using var response = await client.GetAsync(new Uri($"http://{origin.Host}/get")); + + Assert.Equal(HttpStatusCode.ServiceUnavailable, response.StatusCode); + } + [Fact] public async Task GenericRandomError_DynamicRetryAfterOnNon429_IsResolved() { diff --git a/DevProxy.Integration.Tests/ManipulationAndMockingIntegrationTests.cs b/DevProxy.Integration.Tests/ManipulationAndMockingIntegrationTests.cs index c698aefc..9abdb867 100644 --- a/DevProxy.Integration.Tests/ManipulationAndMockingIntegrationTests.cs +++ b/DevProxy.Integration.Tests/ManipulationAndMockingIntegrationTests.cs @@ -320,6 +320,43 @@ public async Task MockResponse_ShortCircuitsWithConfiguredMock() Assert.Contains("mocked-by-test", body, StringComparison.Ordinal); } + [Theory] + [InlineData("/Get")] + [InlineData("/Ge*")] + public async Task MockResponse_MatchesUrlCaseInsensitively(string mockPath) + { + await using var origin = await FakeOrigin.StartAsync(); + var urls = KestrelProxyHarness.BuildUrlsToWatch(origin.Host); + + var config = PluginConfig.FromJson($$""" + { + "mocks": [ + { + "request": { "url": "http://{{origin.Host}}{{mockPath}}", "method": "GET" }, + "response": { "statusCode": 201, "body": "mocked-by-test" } + } + ] + } + """); + var plugin = new MockResponsePlugin( + SharedHttpClient, + NullLogger.Instance, + urls, + ProxyConfig, + config); + + await using var proxy = await KestrelProxyHarness.StartAsync( + origin.Host, [plugin]); + using var client = proxy.CreateHttpClient(); + + using var response = await client.GetAsync(new Uri($"http://{origin.Host}/get")); + var body = await response.Content.ReadAsStringAsync(); + + // urlsToWatch matching ignores case, so mock URL matching must too. + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + Assert.Contains("mocked-by-test", body, StringComparison.Ordinal); + } + [Fact] public async Task Auth_ApiKey_RejectsRequestWithoutKey() { diff --git a/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs b/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs index d0ff8a8d..6dbe35be 100644 --- a/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs +++ b/DevProxy.Plugins/Behavior/GenericRandomErrorPlugin.cs @@ -188,7 +188,7 @@ private static ThrottlingInfo ShouldThrottle(IHttpRequest request, string thrott return false; } - if (errorResponse.Request.Url == request.Url && + if (string.Equals(errorResponse.Request.Url, request.Url, StringComparison.OrdinalIgnoreCase) && HasMatchingBody(errorResponse, request)) { return true; @@ -203,7 +203,7 @@ private static ThrottlingInfo ShouldThrottle(IHttpRequest request, string thrott // turn mock URL with wildcard into a regex and match against the request URL var errorResponseUrlRegex = Regex.Escape(errorResponse.Request.Url).Replace("\\*", ".*", StringComparison.OrdinalIgnoreCase); - return Regex.IsMatch(request.Url, $"^{errorResponseUrlRegex}$") && + return Regex.IsMatch(request.Url, $"^{errorResponseUrlRegex}$", RegexOptions.IgnoreCase) && HasMatchingBody(errorResponse, request); }); diff --git a/DevProxy.Plugins/Mocking/GraphMockResponsePlugin.cs b/DevProxy.Plugins/Mocking/GraphMockResponsePlugin.cs index 172ed290..afb6c41b 100644 --- a/DevProxy.Plugins/Mocking/GraphMockResponsePlugin.cs +++ b/DevProxy.Plugins/Mocking/GraphMockResponsePlugin.cs @@ -190,7 +190,7 @@ Configuration.Mocks is null || var absoluteRequestFromBatchUrl = ProxyUtils .GetAbsoluteRequestUrlFromBatch(batchRequestUri, request.Url) .ToString(); - if (mockResponse.Request.Url == absoluteRequestFromBatchUrl) + if (string.Equals(mockResponse.Request.Url, absoluteRequestFromBatchUrl, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -204,7 +204,7 @@ Configuration.Mocks is null || //turn mock URL with wildcard into a regex and match against the request URL var mockResponseUrlRegex = Regex.Escape(mockResponse.Request.Url).Replace("\\*", ".*", StringComparison.OrdinalIgnoreCase); - return Regex.IsMatch(absoluteRequestFromBatchUrl, $"^{mockResponseUrlRegex}$"); + return Regex.IsMatch(absoluteRequestFromBatchUrl, $"^{mockResponseUrlRegex}$", RegexOptions.IgnoreCase); }); return mockResponse; } diff --git a/DevProxy.Plugins/Mocking/MockResponsePlugin.cs b/DevProxy.Plugins/Mocking/MockResponsePlugin.cs index ce115b10..60834207 100644 --- a/DevProxy.Plugins/Mocking/MockResponsePlugin.cs +++ b/DevProxy.Plugins/Mocking/MockResponsePlugin.cs @@ -304,7 +304,7 @@ Configuration.Mocks is null || return false; } - if (mockResponse.Request.Url == request.Url && + if (string.Equals(mockResponse.Request.Url, request.Url, StringComparison.OrdinalIgnoreCase) && HasMatchingBody(mockResponse, request) && IsNthRequest(mockResponse)) { @@ -319,7 +319,7 @@ Configuration.Mocks is null || } // turn mock URL with wildcard into a regex and match against the request URL - return Regex.IsMatch(request.Url, ProxyUtils.PatternToRegex(mockResponse.Request.Url)) && + return Regex.IsMatch(request.Url, ProxyUtils.PatternToRegex(mockResponse.Request.Url), RegexOptions.IgnoreCase) && HasMatchingBody(mockResponse, request) && IsNthRequest(mockResponse); }); diff --git a/DevProxy.Plugins/Mocking/WebSocketMockResponsePlugin.cs b/DevProxy.Plugins/Mocking/WebSocketMockResponsePlugin.cs index cd0b5ee5..83348430 100644 --- a/DevProxy.Plugins/Mocking/WebSocketMockResponsePlugin.cs +++ b/DevProxy.Plugins/Mocking/WebSocketMockResponsePlugin.cs @@ -268,7 +268,7 @@ private static Task SendAsync(IWebSocketConnection connection, WebSocketMessageM return false; } - return Regex.IsMatch(requestUrl, ProxyUtils.PatternToRegex(mockUrl)); + return Regex.IsMatch(requestUrl, ProxyUtils.PatternToRegex(mockUrl), RegexOptions.IgnoreCase); }); } }