Skip to content

Commit 78b9d7f

Browse files
Adds support for mirroring mock responses. Closes #1384
1 parent 3100032 commit 78b9d7f

File tree

1 file changed

+190
-0
lines changed

1 file changed

+190
-0
lines changed

DevProxy.Plugins/Mocking/MockResponsePlugin.cs

Lines changed: 190 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,192 @@ 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<dynamic>(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, dynamic 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, dynamic 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 element.GetDecimal();
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 // Return type must be object because we can return any type from the request
611+
private static object? ReplacePlaceholderInString(string value, dynamic requestBody, ILogger logger)
612+
#pragma warning restore CA1859
613+
{
614+
logger.LogTrace("{Method} called", nameof(ReplacePlaceholderInString));
615+
616+
logger.LogDebug("Processing value: {Value}", value);
617+
618+
// Check if the value starts with @request.body.
619+
if (!value.StartsWith("@request.body.", StringComparison.OrdinalIgnoreCase))
620+
{
621+
logger.LogDebug("Value {Value} does not start with @request.body. Skipping", value);
622+
return value;
623+
}
624+
625+
// Extract the property path after @request.body.
626+
var propertyPath = value["@request.body.".Length..];
627+
628+
logger.LogDebug("Extracted property path: {PropertyPath}", propertyPath);
629+
630+
return GetValueFromRequestBody(requestBody, propertyPath, logger);
631+
}
632+
633+
private static object? GetValueFromRequestBody(dynamic requestBody, string propertyPath, ILogger logger)
634+
{
635+
logger.LogTrace("{Method} called", nameof(GetValueFromRequestBody));
636+
637+
logger.LogDebug("Getting value for {PropertyPath}", propertyPath);
638+
639+
try
640+
{
641+
// Split the property path by dots to handle nested properties
642+
var propertyNames = propertyPath.Split('.');
643+
644+
// Handle JsonElement
645+
if (requestBody is JsonElement element)
646+
{
647+
return GetNestedValueFromJsonElement(element, propertyNames, logger);
648+
}
649+
else
650+
{
651+
// Handle other dynamic types by converting to JsonElement
652+
var json = JsonSerializer.Serialize(requestBody);
653+
var jsonElement = JsonSerializer.Deserialize<JsonElement>(json);
654+
return GetNestedValueFromJsonElement(jsonElement, propertyNames, logger);
655+
}
656+
}
657+
catch
658+
{
659+
// If we can't get the property, return null
660+
}
661+
662+
return null;
663+
}
664+
665+
private static object? GetNestedValueFromJsonElement(JsonElement element, string[] propertyNames, ILogger logger)
666+
{
667+
logger.LogTrace("{Method} called", nameof(GetNestedValueFromJsonElement));
668+
669+
var current = element;
670+
671+
// Navigate through the nested properties
672+
foreach (var propertyName in propertyNames)
673+
{
674+
if (current.ValueKind != JsonValueKind.Object)
675+
{
676+
logger.LogDebug("Current JSON element is not an object. Cannot navigate to property {PropertyName}", propertyName);
677+
return null; // Can't navigate further if current element is not an object
678+
}
679+
680+
if (!current.TryGetProperty(propertyName, out current))
681+
{
682+
logger.LogDebug("Property {PropertyName} not found in JSON. Returning null", propertyName);
683+
return null; // Property not found
684+
}
685+
}
686+
687+
return ConvertJsonElementToObject(current, logger);
688+
}
689+
690+
private static object? ConvertJsonElementToObject(JsonElement element, ILogger logger)
691+
{
692+
logger.LogTrace("{Method} called", nameof(ConvertJsonElementToObject));
693+
694+
return element.ValueKind switch
695+
{
696+
JsonValueKind.String => element.GetString(),
697+
JsonValueKind.Number => element.GetDecimal(),
698+
JsonValueKind.True => true,
699+
JsonValueKind.False => false,
700+
JsonValueKind.Null or JsonValueKind.Undefined => null,
701+
// For complex objects/arrays, return the JsonElement itself
702+
// which can be serialized later
703+
JsonValueKind.Object or JsonValueKind.Array => element,
704+
_ => element.ToString(),
705+
};
706+
}
517707
}

0 commit comments

Comments
 (0)