Skip to content

Commit

Permalink
Makes mocks and errors consistent. Closes #453 (#457)
Browse files Browse the repository at this point in the history
  • Loading branch information
waldekmastykarz authored Dec 22, 2023
1 parent 0c7e1cf commit 63dfa8a
Show file tree
Hide file tree
Showing 17 changed files with 315 additions and 242 deletions.
14 changes: 7 additions & 7 deletions dev-proxy-plugins/Behavior/RateLimitingPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ _urlsToWatch is null ||
_resourcesRemaining = 0;
var request = e.Session.HttpClient.Request;

_logger?.LogRequest(new[] { $"Exceeded resource limit when calling {request.Url}.", "Request will be throttled" }, MessageType.Failed, new LoggingContext(e.Session));
_logger?.LogRequest([$"Exceeded resource limit when calling {request.Url}.", "Request will be throttled"], MessageType.Failed, new LoggingContext(e.Session));
if (_configuration.WhenLimitExceeded == RateLimitResponseWhenLimitExceeded.Throttle)
{
e.ThrottledRequests.Add(new ThrottlerInfo(
Expand All @@ -244,12 +244,12 @@ _urlsToWatch is null ||
{
if (_configuration.CustomResponse is not null)
{
var headers = _configuration.CustomResponse.ResponseHeaders is not null ?
_configuration.CustomResponse.ResponseHeaders.Select(h => new HttpHeader(h.Key, h.Value)) :
var headers = _configuration.CustomResponse.Response?.Headers is not null ?
_configuration.CustomResponse.Response.Headers.Select(h => new HttpHeader(h.Key, h.Value)) :
Array.Empty<HttpHeader>();

// allow custom throttling response
var responseCode = (HttpStatusCode)(_configuration.CustomResponse.ResponseCode ?? 200);
var responseCode = (HttpStatusCode)(_configuration.CustomResponse.Response?.StatusCode ?? 200);
if (responseCode == HttpStatusCode.TooManyRequests)
{
e.ThrottledRequests.Add(new ThrottlerInfo(
Expand All @@ -259,15 +259,15 @@ _urlsToWatch is null ||
));
}

string body = _configuration.CustomResponse.ResponseBody is not null ?
JsonSerializer.Serialize(_configuration.CustomResponse.ResponseBody, new JsonSerializerOptions { WriteIndented = true }) :
string body = _configuration.CustomResponse.Response?.Body is not null ?
JsonSerializer.Serialize(_configuration.CustomResponse.Response.Body, new JsonSerializerOptions { WriteIndented = true }) :
"";
e.Session.GenericResponse(body, responseCode, headers);
state.HasBeenSet = true;
}
else
{
_logger?.LogRequest(new[] { $"Custom behavior not set. {_configuration.CustomResponseFile} not found." }, MessageType.Failed, new LoggingContext(e.Session));
_logger?.LogRequest([$"Custom behavior not set. {_configuration.CustomResponseFile} not found."], MessageType.Failed, new LoggingContext(e.Session));
}
}
}
Expand Down
2 changes: 0 additions & 2 deletions dev-proxy-plugins/GenericErrorResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,4 @@ public class GenericErrorResponse
public Dictionary<string, string>? Headers { get; set; }
[JsonPropertyName("body")]
public dynamic? Body { get; set; }
[JsonPropertyName("addDynamicRetryAfter")]
public bool? AddDynamicRetryAfter { get; set; } = false;
}
34 changes: 17 additions & 17 deletions dev-proxy-plugins/MockResponses/GraphMockResponsePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,22 +62,22 @@ protected override async Task OnRequest(object? sender, ProxyRequestArgs e)
}
};

_logger?.LogRequest(new[] { $"502 {request.Url}" }, MessageType.Mocked, new LoggingContext(e.Session));
_logger?.LogRequest([$"502 {request.Url}"], MessageType.Mocked, new LoggingContext(e.Session));
}
else
{
dynamic? body = null;
var statusCode = HttpStatusCode.OK;
if (mockResponse.ResponseCode is not null)
if (mockResponse.Response?.StatusCode is not null)
{
statusCode = (HttpStatusCode)mockResponse.ResponseCode;
statusCode = (HttpStatusCode)mockResponse.Response.StatusCode;
}

if (mockResponse.ResponseHeaders is not null)
if (mockResponse.Response?.Headers is not null)
{
foreach (var key in mockResponse.ResponseHeaders.Keys)
foreach (var key in mockResponse.Response.Headers.Keys)
{
headers[key] = mockResponse.ResponseHeaders[key];
headers[key] = mockResponse.Response.Headers[key];
}
}
// default the content type to application/json unless set in the mock response
Expand All @@ -86,9 +86,9 @@ protected override async Task OnRequest(object? sender, ProxyRequestArgs e)
headers.Add("content-type", "application/json");
}

if (mockResponse.ResponseBody is not null)
if (mockResponse.Response?.Body is not null)
{
var bodyString = JsonSerializer.Serialize(mockResponse.ResponseBody) as string;
var bodyString = JsonSerializer.Serialize(mockResponse.Response.Body) as string;
// we get a JSON string so need to start with the opening quote
if (bodyString?.StartsWith("\"@") ?? false)
{
Expand All @@ -111,7 +111,7 @@ protected override async Task OnRequest(object? sender, ProxyRequestArgs e)
}
else
{
body = mockResponse.ResponseBody;
body = mockResponse.Response.Body;
}
}
response = new GraphBatchResponsePayloadResponse
Expand All @@ -122,7 +122,7 @@ protected override async Task OnRequest(object? sender, ProxyRequestArgs e)
Body = body
};

_logger?.LogRequest(new[] { $"{mockResponse.ResponseCode ?? 200} {mockResponse.Url}" }, MessageType.Mocked, new LoggingContext(e.Session));
_logger?.LogRequest([$"{mockResponse.Response?.StatusCode ?? 200} {mockResponse.Request?.Url}"], MessageType.Mocked, new LoggingContext(e.Session));
}

responses.Add(response);
Expand All @@ -143,34 +143,34 @@ protected override async Task OnRequest(object? sender, ProxyRequestArgs e)
protected MockResponse? GetMatchingMockResponse(GraphBatchRequestPayloadRequest request, Uri batchRequestUri)
{
if (_configuration.NoMocks ||
_configuration.Responses is null ||
!_configuration.Responses.Any())
_configuration.Mocks is null ||
!_configuration.Mocks.Any())
{
return null;
}

var mockResponse = _configuration.Responses.FirstOrDefault(mockResponse =>
var mockResponse = _configuration.Mocks.FirstOrDefault(mockResponse =>
{
if (mockResponse.Method != request.Method) return false;
if (mockResponse.Request?.Method != request.Method) return false;
// URLs in batch are relative to Graph version number so we need
// to make them absolute using the batch request URL
var absoluteRequestFromBatchUrl = ProxyUtils
.GetAbsoluteRequestUrlFromBatch(batchRequestUri, request.Url)
.ToString();
if (mockResponse.Url == absoluteRequestFromBatchUrl)
if (mockResponse.Request.Url == absoluteRequestFromBatchUrl)
{
return true;
}

// check if the URL contains a wildcard
// if it doesn't, it's not a match for the current request for sure
if (!mockResponse.Url.Contains('*'))
if (!mockResponse.Request.Url.Contains('*'))
{
return false;
}

//turn mock URL with wildcard into a regex and match against the request URL
var mockResponseUrlRegex = Regex.Escape(mockResponse.Url).Replace("\\*", ".*");
var mockResponseUrlRegex = Regex.Escape(mockResponse.Request.Url).Replace("\\*", ".*");
return Regex.IsMatch(absoluteRequestFromBatchUrl, $"^{mockResponseUrlRegex}$");
});
return mockResponse;
Expand Down
24 changes: 18 additions & 6 deletions dev-proxy-plugins/MockResponses/MockResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,29 @@
namespace Microsoft.DevProxy.Plugins.MockResponses;

public class MockResponse
{
[JsonPropertyName("request")]
public MockResponseRequest? Request { get; set; }
[JsonPropertyName("response")]
public MockResponseResponse? Response { get; set; }
}

public class MockResponseRequest
{
[JsonPropertyName("url")]
public string Url { get; set; } = string.Empty;
[JsonPropertyName("method")]
public string Method { get; set; } = "GET";
[JsonPropertyName("nth"), JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public int? Nth { get; set; }
[JsonPropertyName("responseCode")]
public int? ResponseCode { get; set; } = 200;
[JsonPropertyName("responseBody")]
public dynamic? ResponseBody { get; set; }
[JsonPropertyName("responseHeaders")]
public IDictionary<string, string>? ResponseHeaders { get; set; }
}

public class MockResponseResponse
{
[JsonPropertyName("statusCode")]
public int? StatusCode { get; set; } = 200;
[JsonPropertyName("body")]
public dynamic? Body { get; set; }
[JsonPropertyName("headers")]
public IDictionary<string, string>? Headers { get; set; }
}
84 changes: 47 additions & 37 deletions dev-proxy-plugins/MockResponses/MockResponsePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@ public class MockResponseConfiguration
[JsonIgnore]
public bool NoMocks { get; set; } = false;
[JsonIgnore]
public string MocksFile { get; set; } = "responses.json";
public string MocksFile { get; set; } = "mocks.json";
[JsonIgnore]
public bool BlockUnmockedRequests { get; set; } = false;

[JsonPropertyName("$schema")]
public string Schema { get; set; } = "https://raw.githubusercontent.com/microsoft/dev-proxy/main/schemas/v1.0/responses.schema.json";
[JsonPropertyName("responses")]
public IEnumerable<MockResponse> Responses { get; set; } = Array.Empty<MockResponse>();
public string Schema { get; set; } = "https://raw.githubusercontent.com/microsoft/dev-proxy/main/schemas/v1.0/mockresponseplugin.schema.json";
[JsonPropertyName("mocks")]
public IEnumerable<MockResponse> Mocks { get; set; } = Array.Empty<MockResponse>();
}

public class MockResponsePlugin : BaseProxyPlugin
Expand All @@ -48,8 +48,10 @@ public MockResponsePlugin()
_noMocks.AddAlias("-n");
_noMocks.ArgumentHelpName = "no mocks";

_mocksFile = new Option<string?>("--mocks-file", "Provide a file populated with mock responses");
_mocksFile.ArgumentHelpName = "mocks file";
_mocksFile = new Option<string?>("--mocks-file", "Provide a file populated with mock responses")
{
ArgumentHelpName = "mocks file"
};
}

public override void Register(IPluginEvents pluginEvents,
Expand Down Expand Up @@ -120,14 +122,20 @@ protected virtual Task OnRequest(object? sender, ProxyRequestArgs e)
{
ProcessMockResponse(e.Session, new MockResponse
{
Method = request.Method,
Url = request.Url,
ResponseCode = 502,
ResponseBody = new GraphErrorResponseBody(new GraphErrorResponseError
Request = new()
{
Url = request.Url,
Method = request.Method
},
Response = new()
{
Code = "Bad Gateway",
Message = $"No mock response found for {request.Method} {request.Url}"
})
StatusCode = 502,
Body = new GraphErrorResponseBody(new GraphErrorResponseError
{
Code = "Bad Gateway",
Message = $"No mock response found for {request.Method} {request.Url}"
})
}
});
state.HasBeenSet = true;
}
Expand All @@ -139,57 +147,59 @@ protected virtual Task OnRequest(object? sender, ProxyRequestArgs e)
private MockResponse? GetMatchingMockResponse(Request request)
{
if (_configuration.NoMocks ||
_configuration.Responses is null ||
!_configuration.Responses.Any())
_configuration.Mocks is null ||
!_configuration.Mocks.Any())
{
return null;
}

var mockResponse = _configuration.Responses.FirstOrDefault(mockResponse =>
var mockResponse = _configuration.Mocks.FirstOrDefault(mockResponse =>
{
if (mockResponse.Method != request.Method) return false;
if (mockResponse.Url == request.Url && IsNthRequest(mockResponse))
if (mockResponse.Request is null) return false;

if (mockResponse.Request.Method != request.Method) return false;
if (mockResponse.Request.Url == request.Url && IsNthRequest(mockResponse))
{
return true;
}

// check if the URL contains a wildcard
// if it doesn't, it's not a match for the current request for sure
if (!mockResponse.Url.Contains('*'))
if (!mockResponse.Request.Url.Contains('*'))
{
return false;
}

//turn mock URL with wildcard into a regex and match against the request URL
var mockResponseUrlRegex = Regex.Escape(mockResponse.Url).Replace("\\*", ".*");
var mockResponseUrlRegex = Regex.Escape(mockResponse.Request.Url).Replace("\\*", ".*");
return Regex.IsMatch(request.Url, $"^{mockResponseUrlRegex}$") && IsNthRequest(mockResponse);
});

if (mockResponse is not null)
if (mockResponse is not null && mockResponse.Request is not null)
{
if (!_appliedMocks.ContainsKey(mockResponse.Url))
if (!_appliedMocks.ContainsKey(mockResponse.Request.Url))
{
_appliedMocks.Add(mockResponse.Url, 0);
_appliedMocks.Add(mockResponse.Request.Url, 0);
}
_appliedMocks[mockResponse.Url]++;
_appliedMocks[mockResponse.Request.Url]++;
}

return mockResponse;
}

private bool IsNthRequest(MockResponse mockResponse)
{
if (mockResponse.Nth is null)
if (mockResponse.Request is null || mockResponse.Request.Nth is null)
{
// mock doesn't define an Nth property so it always qualifies
return true;
}

var nth = 0;
_appliedMocks.TryGetValue(mockResponse.Url, out nth);
int nth;
_appliedMocks.TryGetValue(mockResponse.Request.Url, out nth);
nth++;

return mockResponse.Nth == nth;
return mockResponse.Request.Nth == nth;
}

private void ProcessMockResponse(SessionEventArgs e, MockResponse matchingResponse)
Expand All @@ -199,14 +209,14 @@ private void ProcessMockResponse(SessionEventArgs e, MockResponse matchingRespon
string requestDate = DateTime.Now.ToString();
var headers = ProxyUtils.BuildGraphResponseHeaders(e.HttpClient.Request, requestId, requestDate);
HttpStatusCode statusCode = HttpStatusCode.OK;
if (matchingResponse.ResponseCode is not null)
if (matchingResponse.Response?.StatusCode is not null)
{
statusCode = (HttpStatusCode)matchingResponse.ResponseCode;
statusCode = (HttpStatusCode)matchingResponse.Response.StatusCode;
}

if (matchingResponse.ResponseHeaders is not null)
if (matchingResponse.Response?.Headers is not null)
{
foreach (var key in matchingResponse.ResponseHeaders.Keys)
foreach (var key in matchingResponse.Response.Headers.Keys)
{
// remove duplicate headers
var existingHeader = headers.FirstOrDefault(h => h.Name.Equals(key, StringComparison.OrdinalIgnoreCase));
Expand All @@ -215,7 +225,7 @@ private void ProcessMockResponse(SessionEventArgs e, MockResponse matchingRespon
headers.Remove(existingHeader);
}

headers.Add(new HttpHeader(key, matchingResponse.ResponseHeaders[key]));
headers.Add(new HttpHeader(key, matchingResponse.Response.Headers[key]));
}
}
// default the content type to application/json unless set in the mock response
Expand All @@ -224,9 +234,9 @@ private void ProcessMockResponse(SessionEventArgs e, MockResponse matchingRespon
headers.Add(new HttpHeader("content-type", "application/json"));
}

if (matchingResponse.ResponseBody is not null)
if (matchingResponse.Response?.Body is not null)
{
var bodyString = JsonSerializer.Serialize(matchingResponse.ResponseBody) as string;
var bodyString = JsonSerializer.Serialize(matchingResponse.Response.Body) as string;
// we get a JSON string so need to start with the opening quote
if (bodyString?.StartsWith("\"@") ?? false)
{
Expand All @@ -245,7 +255,7 @@ private void ProcessMockResponse(SessionEventArgs e, MockResponse matchingRespon
{
var bodyBytes = File.ReadAllBytes(filePath);
e.GenericResponse(bodyBytes, statusCode, headers);
_logger?.LogRequest(new[] { $"{matchingResponse.ResponseCode ?? 200} {matchingResponse.Url}" }, MessageType.Mocked, new LoggingContext(e));
_logger?.LogRequest([$"{matchingResponse.Response.StatusCode ?? 200} {matchingResponse.Request?.Url}"], MessageType.Mocked, new LoggingContext(e));
return;
}
}
Expand All @@ -256,6 +266,6 @@ private void ProcessMockResponse(SessionEventArgs e, MockResponse matchingRespon
}
e.GenericResponse(body ?? string.Empty, statusCode, headers);

_logger?.LogRequest(new[] { $"{matchingResponse.ResponseCode ?? 200} {matchingResponse.Url}" }, MessageType.Mocked, new LoggingContext(e));
_logger?.LogRequest([$"{matchingResponse.Response?.StatusCode ?? 200} {matchingResponse.Request?.Url}"], MessageType.Mocked, new LoggingContext(e));
}
}
Loading

0 comments on commit 63dfa8a

Please sign in to comment.