Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<GraphRandomErrorPlugin>.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);
}
}
34 changes: 28 additions & 6 deletions DevProxy.Plugins/Behavior/GraphRandomErrorPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -208,23 +213,39 @@ 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();

var batch = JsonSerializer.Deserialize<GraphBatchRequestPayload>(e.ProxySession.Request.BodyString, ProxyUtils.JsonSerializerOptions);
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<GraphBatchResponsePayloadResponse>();
Expand Down Expand Up @@ -276,6 +297,7 @@ private void FailBatch(ProxyRequestArgs e)
batchResponse.Responses = [.. responses];

UpdateProxyBatchResponse(e, batchResponse);
return true;
}

private ThrottlingInfo ShouldThrottle(IHttpRequest request, string throttlingKey)
Expand Down
Loading