Compare json object values instead of byte streams when matching bodies that are content-type `json` (#8860)

This commit is contained in:
Scott Beddall 2024-08-25 11:35:04 -07:00 коммит произвёл GitHub
Родитель 4aea26ad99
Коммит c597728040
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
4 изменённых файлов: 208 добавлений и 22 удалений

Просмотреть файл

@ -8,7 +8,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@ -689,8 +688,10 @@ namespace Azure.Sdk.Tools.TestProxy.Tests
Assert.Equal(0, matcher.CompareHeaderDictionaries(targetUntouchedEntry.Request.Headers, targetEntry.Request.Headers, new HashSet<string>(), new HashSet<string>()));
Assert.Equal(0, matcher.CompareHeaderDictionaries(targetUntouchedEntry.Response.Headers, targetEntry.Response.Headers, new HashSet<string>(), new HashSet<string>()));
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Request.Body, targetEntry.Request.Body));
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Response.Body, targetEntry.Response.Body));
targetUntouchedEntry.Request.TryGetContentType(out var contentType);
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Request.Body, targetEntry.Request.Body, contentType));
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Response.Body, targetEntry.Response.Body, contentType));
Assert.Equal(targetUntouchedEntry.RequestUri, targetEntry.RequestUri);
}
@ -769,8 +770,9 @@ namespace Azure.Sdk.Tools.TestProxy.Tests
await session.Session.Sanitize(sanitizer);
var resultBodyValue = Encoding.UTF8.GetString(targetEntry.Request.Body);
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Request.Body, targetEntry.Request.Body));
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Response.Body, targetEntry.Response.Body));
targetUntouchedEntry.Request.TryGetContentType(out var contentType);
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Request.Body, targetEntry.Request.Body, contentType));
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Response.Body, targetEntry.Response.Body, contentType));
}
[Fact]

Просмотреть файл

@ -0,0 +1,157 @@
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using System.Text.Json;
namespace Azure.Sdk.Tools.TestProxy.Common
{
public class JsonComparer
{
public static List<string> CompareJson(byte[] json1, byte[] json2)
{
var differences = new List<string>();
JsonDocument doc1;
JsonDocument doc2;
// Deserialize the byte arrays to JsonDocument
try
{
doc1 = JsonDocument.Parse(json1);
}
catch(Exception ex)
{
differences.Add($"Unable to parse the request json body. Content \"{Encoding.UTF8.GetString(json1)}.\" Exception: {ex.Message}");
return differences;
}
try
{
doc2 = JsonDocument.Parse(json2);
}
catch (Exception ex)
{
differences.Add($"Unable to parse the record json body. Content \"{Encoding.UTF8.GetString(json2)}.\" Exception: {ex.Message}");
return differences;
}
CompareElements(doc1.RootElement, doc2.RootElement, differences, "");
return differences;
}
private static void CompareElements(JsonElement element1, JsonElement element2, List<string> differences, string path)
{
if (element1.ValueKind != element2.ValueKind)
{
differences.Add($"{path}: Request and record have different types.");
return;
}
switch (element1.ValueKind)
{
case JsonValueKind.Object:
{
var properties1 = element1.EnumerateObject();
var properties2 = element2.EnumerateObject();
var propDict1 = new Dictionary<string, JsonElement>();
var propDict2 = new Dictionary<string, JsonElement>();
foreach (var prop in properties1)
propDict1[prop.Name] = prop.Value;
foreach (var prop in properties2)
propDict2[prop.Name] = prop.Value;
foreach (var key in propDict1.Keys)
{
if (propDict2.ContainsKey(key))
{
CompareElements(propDict1[key], propDict2[key], differences, $"{path}.{key}");
}
else
{
differences.Add($"{path}.{key}: Missing in request JSON");
}
}
foreach (var key in propDict2.Keys)
{
if (!propDict1.ContainsKey(key))
{
differences.Add($"{path}.{key}: Missing in record JSON");
}
}
break;
}
case JsonValueKind.Array:
{
var array1 = element1.EnumerateArray();
var array2 = element2.EnumerateArray();
int index = 0;
var enum1 = array1.GetEnumerator();
var enum2 = array2.GetEnumerator();
while (enum1.MoveNext() && enum2.MoveNext())
{
CompareElements(enum1.Current, enum2.Current, differences, $"{path}[{index}]");
index++;
}
while (enum1.MoveNext())
{
differences.Add($"{path}[{index}]: Extra element in request JSON");
index++;
}
while (enum2.MoveNext())
{
differences.Add($"{path}[{index}]: Extra element in record JSON");
index++;
}
break;
}
case JsonValueKind.String:
{
if (element1.GetString() != element2.GetString())
{
differences.Add($"{path}: \"{element1.GetString()}\" != \"{element2.GetString()}\"");
}
break;
}
case JsonValueKind.Number:
{
if (element1.GetDecimal() != element2.GetDecimal())
{
differences.Add($"{path}: {element1.GetDecimal()} != {element2.GetDecimal()}");
}
break;
}
case JsonValueKind.True:
case JsonValueKind.False:
{
if (element1.GetBoolean() != element2.GetBoolean())
{
differences.Add($"{path}: {element1.GetBoolean()} != {element2.GetBoolean()}");
}
break;
}
case JsonValueKind.Null:
{
// Both are null, nothing to compare
break;
}
default:
{
differences.Add($"{path}: Unhandled value kind {element1.ValueKind}");
break;
}
}
}
}
}

Просмотреть файл

@ -112,7 +112,10 @@ namespace Azure.Sdk.Tools.TestProxy.Common
if (!entry.IsTrack1Recording)
{
score += CompareHeaderDictionaries(request.Request.Headers, entry.Request.Headers, IgnoredHeaders, ExcludeHeaders);
score += CompareBodies(request.Request.Body, entry.Request.Body);
request.Request.TryGetContentType(out var contentType);
score += CompareBodies(request.Request.Body, entry.Request.Body, descriptionBuilder: null, contentType: contentType);
}
if (score == 0)
@ -130,7 +133,7 @@ namespace Azure.Sdk.Tools.TestProxy.Common
throw new TestRecordingMismatchException(GenerateException(request, bestScoreEntry, entries));
}
public virtual int CompareBodies(byte[] requestBody, byte[] recordBody, StringBuilder descriptionBuilder = null)
public virtual int CompareBodies(byte[] requestBody, byte[] recordBody, string contentType, StringBuilder descriptionBuilder = null)
{
if (!_compareBodies)
{
@ -154,27 +157,50 @@ namespace Azure.Sdk.Tools.TestProxy.Common
return 1;
}
if (!requestBody.SequenceEqual(recordBody))
{
if (descriptionBuilder != null)
// we just failed sequence equality, before erroring, lets check if we're a json body and check for property equality
if (!string.IsNullOrWhiteSpace(contentType) && contentType.Contains("json"))
{
var minLength = Math.Min(requestBody.Length, recordBody.Length);
int i;
for (i = 0; i < minLength - 1; i++)
var jsonDifferences = JsonComparer.CompareJson(requestBody, recordBody);
if (jsonDifferences.Count > 0)
{
if (requestBody[i] != recordBody[i])
if (descriptionBuilder != null)
{
break;
descriptionBuilder.AppendLine($"There are differences between request and recordentry bodies:");
foreach (var jsonDifference in jsonDifferences)
{
descriptionBuilder.AppendLine(jsonDifference);
}
}
return 1;
}
descriptionBuilder.AppendLine($"Request and record bodies do not match at index {i}:");
var before = Math.Max(0, i - 10);
var afterRequest = Math.Min(i + 20, requestBody.Length);
var afterResponse = Math.Min(i + 20, recordBody.Length);
descriptionBuilder.AppendLine($" request: \"{Encoding.UTF8.GetString(requestBody, before, afterRequest - before)}\"");
descriptionBuilder.AppendLine($" record: \"{Encoding.UTF8.GetString(recordBody, before, afterResponse - before)}\"");
}
else {
if (descriptionBuilder != null)
{
var minLength = Math.Min(requestBody.Length, recordBody.Length);
int i;
for (i = 0; i < minLength - 1; i++)
{
if (requestBody[i] != recordBody[i])
{
break;
}
}
descriptionBuilder.AppendLine($"Request and record bodies do not match at index {i}:");
var before = Math.Max(0, i - 10);
var afterRequest = Math.Min(i + 20, requestBody.Length);
var afterResponse = Math.Min(i + 20, recordBody.Length);
descriptionBuilder.AppendLine($" request: \"{Encoding.UTF8.GetString(requestBody, before, afterRequest - before)}\"");
descriptionBuilder.AppendLine($" record: \"{Encoding.UTF8.GetString(recordBody, before, afterResponse - before)}\"");
}
return 1;
}
return 1;
}
return 0;
@ -250,7 +276,8 @@ namespace Azure.Sdk.Tools.TestProxy.Common
builder.AppendLine("Body differences:");
CompareBodies(request.Request.Body, bestScoreEntry.Request.Body, builder);
request.Request.TryGetContentType(out var contentType);
CompareBodies(request.Request.Body, bestScoreEntry.Request.Body, contentType, descriptionBuilder: builder);
return builder.ToString();
}

Просмотреть файл

@ -102,10 +102,10 @@ namespace Azure.Sdk.Tools.TestProxy.Common
{
sanitizer.Sanitize(requestEntry);
}
// normalize request body with STJ using relaxed escaping to match behavior when Deserializing from session files
RecordEntry.NormalizeJsonBody(requestEntry.Request);
RecordEntry entry = matcher.FindMatch(requestEntry, Entries);
if (remove)
{