Skip to content

Commit 8746314

Browse files
Adds support for mirroring mock responses. Closes #1384 (#1386)
1 parent 3100032 commit 8746314

File tree

1 file changed

+207
-0
lines changed

1 file changed

+207
-0
lines changed

DevProxy.Plugins/Mocking/MockResponsePlugin.cs

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,8 @@ private void ProcessMockResponseInternal(ProxyRequestArgs e, MockResponse matchi
374374
ProxyUtils.MergeHeaders(headers, rateLimitingHeaders);
375375
}
376376

377+
ReplacePlaceholders(matchingResponse.Response, e.Session.HttpClient.Request, Logger);
378+
377379
if (matchingResponse.Response?.Body is not null)
378380
{
379381
var bodyString = JsonSerializer.Serialize(matchingResponse.Response.Body, ProxyUtils.JsonSerializerOptions) as string;
@@ -514,4 +516,209 @@ private static bool HasMatchingBody(MockResponse mockResponse, Request request)
514516

515517
return request.BodyString.Contains(mockResponse.Request.BodyFragment, StringComparison.OrdinalIgnoreCase);
516518
}
519+
520+
private static void ReplacePlaceholders(MockResponseResponse? response, Request request, ILogger logger)
521+
{
522+
logger.LogTrace("{Method} called", nameof(ReplacePlaceholders));
523+
524+
if (response is null ||
525+
response.Body is null || request.BodyString is null)
526+
{
527+
logger.LogTrace("Body is empty. Skipping replacing placeholders");
528+
return;
529+
}
530+
531+
try
532+
{
533+
var requestBody = JsonSerializer.Deserialize<JsonElement>(request.BodyString, ProxyUtils.JsonSerializerOptions);
534+
535+
response.Body = ReplacePlaceholdersInObject(response.Body, requestBody, logger);
536+
}
537+
catch (Exception ex)
538+
{
539+
logger.LogDebug(ex, "Failed to parse request body as JSON");
540+
logger.LogWarning("Failed to parse request body as JSON. Placeholders in the mock response won't be replaced.");
541+
}
542+
543+
logger.LogTrace("Left {Method}", nameof(ReplacePlaceholders));
544+
}
545+
546+
private static object? ReplacePlaceholdersInObject(object? obj, JsonElement requestBody, ILogger logger)
547+
{
548+
logger.LogTrace("{Method} called", nameof(ReplacePlaceholdersInObject));
549+
550+
if (obj is null)
551+
{
552+
return null;
553+
}
554+
555+
// Handle JsonElement (which is what we get from System.Text.Json)
556+
if (obj is JsonElement element)
557+
{
558+
return ReplacePlaceholdersInJsonElement(element, requestBody, logger);
559+
}
560+
561+
// Handle string values - check for placeholders
562+
if (obj is string strValue)
563+
{
564+
return ReplacePlaceholderInString(strValue, requestBody, logger);
565+
}
566+
567+
// For other types, convert to JsonElement and process
568+
var json = JsonSerializer.Serialize(obj);
569+
var jsonElement = JsonSerializer.Deserialize<JsonElement>(json);
570+
return ReplacePlaceholdersInJsonElement(jsonElement, requestBody, logger);
571+
}
572+
573+
private static object? ReplacePlaceholdersInJsonElement(JsonElement element, JsonElement requestBody, ILogger logger)
574+
{
575+
logger.LogTrace("{Method} called", nameof(ReplacePlaceholdersInJsonElement));
576+
577+
switch (element.ValueKind)
578+
{
579+
case JsonValueKind.Object:
580+
var resultObj = new Dictionary<string, object?>();
581+
foreach (var property in element.EnumerateObject())
582+
{
583+
resultObj[property.Name] = ReplacePlaceholdersInJsonElement(property.Value, requestBody, logger);
584+
}
585+
return resultObj;
586+
587+
case JsonValueKind.Array:
588+
var resultArray = new List<object?>();
589+
foreach (var item in element.EnumerateArray())
590+
{
591+
resultArray.Add(ReplacePlaceholdersInJsonElement(item, requestBody, logger));
592+
}
593+
return resultArray;
594+
case JsonValueKind.String:
595+
return ReplacePlaceholderInString(element.GetString() ?? "", requestBody, logger);
596+
case JsonValueKind.Number:
597+
return GetSafeNumber(element, logger);
598+
case JsonValueKind.True:
599+
return true;
600+
case JsonValueKind.False:
601+
return false;
602+
case JsonValueKind.Null:
603+
case JsonValueKind.Undefined:
604+
return null;
605+
default:
606+
return element.ToString();
607+
}
608+
}
609+
610+
#pragma warning disable CA1859
611+
// CA1859: This method must return object? because it may return different concrete types (string, int, bool, etc.) based on the JSON content.
612+
private static object? ReplacePlaceholderInString(string value, JsonElement requestBody, ILogger logger)
613+
#pragma warning restore CA1859
614+
{
615+
logger.LogTrace("{Method} called", nameof(ReplacePlaceholderInString));
616+
617+
logger.LogDebug("Processing value: {Value}", value);
618+
619+
// Check if the value starts with @request.body.
620+
if (!value.StartsWith("@request.body.", StringComparison.OrdinalIgnoreCase))
621+
{
622+
logger.LogDebug("Value {Value} does not start with @request.body. Skipping", value);
623+
return value;
624+
}
625+
626+
// Extract the property path after @request.body.
627+
var propertyPath = value["@request.body.".Length..];
628+
629+
logger.LogDebug("Extracted property path: {PropertyPath}", propertyPath);
630+
631+
return GetValueFromRequestBody(requestBody, propertyPath, logger);
632+
}
633+
634+
private static object? GetValueFromRequestBody(JsonElement requestBody, string propertyPath, ILogger logger)
635+
{
636+
logger.LogTrace("{Method} called", nameof(GetValueFromRequestBody));
637+
638+
logger.LogDebug("Getting value for {PropertyPath}", propertyPath);
639+
640+
try
641+
{
642+
// Split the property path by dots to handle nested properties
643+
var propertyNames = propertyPath.Split('.');
644+
return GetNestedValueFromJsonElement(requestBody, propertyNames, logger);
645+
}
646+
catch (Exception ex)
647+
{
648+
// If we can't get the property, return null
649+
logger.LogDebug(ex, "Failed to get value for {PropertyPath}. Returning null", propertyPath);
650+
}
651+
652+
return null;
653+
}
654+
655+
private static object? GetNestedValueFromJsonElement(JsonElement element, string[] propertyNames, ILogger logger)
656+
{
657+
logger.LogTrace("{Method} called", nameof(GetNestedValueFromJsonElement));
658+
659+
var current = element;
660+
661+
// Navigate through the nested properties
662+
foreach (var propertyName in propertyNames)
663+
{
664+
if (current.ValueKind != JsonValueKind.Object)
665+
{
666+
logger.LogDebug("Current JSON element is not an object. Cannot navigate to property {PropertyName}", propertyName);
667+
return null; // Can't navigate further if current element is not an object
668+
}
669+
670+
if (!current.TryGetProperty(propertyName, out current))
671+
{
672+
logger.LogDebug("Property {PropertyName} not found in JSON. Returning null", propertyName);
673+
return null; // Property not found
674+
}
675+
}
676+
677+
return ConvertJsonElementToObject(current, logger);
678+
}
679+
680+
private static object? ConvertJsonElementToObject(JsonElement element, ILogger logger)
681+
{
682+
logger.LogTrace("{Method} called", nameof(ConvertJsonElementToObject));
683+
684+
return element.ValueKind switch
685+
{
686+
JsonValueKind.String => element.GetString(),
687+
JsonValueKind.Number => GetSafeNumber(element, logger),
688+
JsonValueKind.True => true,
689+
JsonValueKind.False => false,
690+
JsonValueKind.Null or JsonValueKind.Undefined => null,
691+
// For complex objects/arrays, return the JsonElement itself
692+
// which can be serialized later
693+
JsonValueKind.Object or JsonValueKind.Array => element,
694+
_ => element.ToString(),
695+
};
696+
}
697+
698+
// Attempts to safely extract a number from a JsonElement, falling back to double or string if necessary
699+
private static object? GetSafeNumber(JsonElement element, ILogger logger)
700+
{
701+
logger.LogTrace("{Method} called", nameof(GetSafeNumber));
702+
703+
// Try to get as int
704+
if (element.TryGetInt32(out var intValue))
705+
{
706+
return intValue;
707+
}
708+
if (element.TryGetInt64(out var longValue))
709+
{
710+
return longValue;
711+
}
712+
if (element.TryGetDecimal(out var decimalValue))
713+
{
714+
return decimalValue;
715+
}
716+
if (element.TryGetDouble(out var doubleValue))
717+
{
718+
return doubleValue;
719+
}
720+
721+
// Fallback: return as string to avoid exceptions
722+
return element.GetRawText();
723+
}
517724
}

0 commit comments

Comments
 (0)