diff --git a/DevProxy.Integration.Tests/ManipulationAndMockingIntegrationTests.cs b/DevProxy.Integration.Tests/ManipulationAndMockingIntegrationTests.cs index 9abdb867..66d6df47 100644 --- a/DevProxy.Integration.Tests/ManipulationAndMockingIntegrationTests.cs +++ b/DevProxy.Integration.Tests/ManipulationAndMockingIntegrationTests.cs @@ -2,9 +2,11 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System.CommandLine; using System.Net; using System.Net.Sockets; using System.Text; +using DevProxy.Abstractions.Proxy; using DevProxy.Plugins.Behavior; using DevProxy.Plugins.Manipulation; using DevProxy.Plugins.Mocking; @@ -450,4 +452,57 @@ public async Task GraphRandomError_Rate100_FailsEveryMatchedRequest() (int)response.StatusCode >= 400, $"Expected an injected 4xx/5xx error, saw {(int)response.StatusCode}."); } + + private static GraphRandomErrorPlugin CreateGraphRandomErrorPlugin(string configJson) + { + var plugin = new GraphRandomErrorPlugin( + SharedHttpClient, + NullLogger.Instance, + KestrelProxyHarness.BuildUrlsToWatch("graph.microsoft.com"), + ProxyConfig, + PluginConfig.FromJson(configJson)); + var command = new RootCommand(); + foreach (var option in plugin.GetOptions()) + { + command.Options.Add(option); + } + plugin.OptionsLoaded(new OptionsLoadedArgs(command.Parse([]))); + return plugin; + } + + [Fact] + public async Task GraphRandomError_NoAllowedErrorsForMethod_PassesThrough() + { + // 507 isn't a built-in error for GET, so GET has no errors left to + // throw while POST does. + var plugin = CreateGraphRandomErrorPlugin("""{ "rate": 100, "allowedErrors": [507] }"""); + + var get = TestExchange.Request("GET", "https://graph.microsoft.com/v1.0/me"); + await plugin.BeforeRequestAsync(get.RequestArgs, CancellationToken.None); + Assert.False(get.State.HasBeenSet); + + var post = TestExchange.Request("POST", "https://graph.microsoft.com/v1.0/me/messages", body: "{}"); + await plugin.BeforeRequestAsync(post.RequestArgs, CancellationToken.None); + Assert.True(post.State.HasBeenSet); + } + + [Fact] + public async Task GraphRandomError_BatchWithMethodWithoutAllowedErrors_PassesThrough() + { + var plugin = CreateGraphRandomErrorPlugin("""{ "rate": 100, "allowedErrors": [507] }"""); + + var batch = TestExchange.Request( + "POST", + "https://graph.microsoft.com/v1.0/$batch", + body: """ + { + "requests": [ + { "id": "1", "method": "GET", "url": "/me" }, + { "id": "2", "method": "POST", "url": "/me/messages", "body": {}, "headers": { "content-type": "application/json" } } + ] + } + """); + await plugin.BeforeRequestAsync(batch.RequestArgs, CancellationToken.None); + Assert.False(batch.State.HasBeenSet); + } } diff --git a/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs b/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs index 1af35fa0..11372aee 100644 --- a/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs +++ b/DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs @@ -193,11 +193,16 @@ public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken ca } if (ProxyUtils.IsGraphBatchUrl(e.ProxySession.Request.RequestUri)) { - FailBatch(e); + if (!FailBatch(e)) + { + Logger.LogRequest("No allowed errors for one or more batch request methods", MessageType.Skipped, new LoggingContext(e.ProxySession)); + return Task.CompletedTask; + } } - else + else if (!FailResponse(e)) { - FailResponse(e); + Logger.LogRequest($"No allowed errors for {e.ProxySession.Request.Method} requests", MessageType.Skipped, new LoggingContext(e.ProxySession)); + return Task.CompletedTask; } state.HasBeenSet = true; @@ -208,15 +213,24 @@ public override Task BeforeRequestAsync(ProxyRequestArgs e, CancellationToken ca // uses config to determine if a request should be failed private GraphRandomErrorFailMode ShouldFail() => _random.Next(0, 100) < Configuration.Rate ? GraphRandomErrorFailMode.Random : GraphRandomErrorFailMode.PassThru; - private void FailResponse(ProxyRequestArgs e) + private bool HasAllowedErrors(string method) => + _methodStatusCode.TryGetValue(method, out var statusCodes) && statusCodes.Length > 0; + + private bool FailResponse(ProxyRequestArgs e) { + if (!HasAllowedErrors(e.ProxySession.Request.Method)) + { + return false; + } + // pick a random error response for the current request method var methodStatusCodes = _methodStatusCode[e.ProxySession.Request.Method]; var errorStatus = methodStatusCodes[_random.Next(0, methodStatusCodes.Length)]; UpdateProxyResponse(e, errorStatus); + return true; } - private void FailBatch(ProxyRequestArgs e) + private bool FailBatch(ProxyRequestArgs e) { var batchResponse = new GraphBatchResponsePayload(); @@ -224,7 +238,14 @@ private void FailBatch(ProxyRequestArgs e) if (batch == null) { UpdateProxyBatchResponse(e, batchResponse); - return; + return true; + } + + // a batch response must contain a response for every request, so if + // any request can't be failed, pass the whole batch through + if (batch.Requests.Any(r => !HasAllowedErrors(r.Method))) + { + return false; } var responses = new List(); @@ -276,6 +297,7 @@ private void FailBatch(ProxyRequestArgs e) batchResponse.Responses = [.. responses]; UpdateProxyBatchResponse(e, batchResponse); + return true; } private ThrottlingInfo ShouldThrottle(IHttpRequest request, string throttlingKey)