Skip to content

Commit c597728

Browse files
authored
Compare json object values instead of byte streams when matching bodies that are content-type json (#8860)
1 parent 4aea26a commit c597728

4 files changed

Lines changed: 208 additions & 22 deletions

File tree

tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/SanitizerTests.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
using System.IO;
99
using System.Linq;
1010
using System.Reflection;
11-
using System.Runtime.CompilerServices;
1211
using System.Text;
1312
using System.Text.RegularExpressions;
1413
using System.Threading.Tasks;
@@ -689,8 +688,10 @@ public async void GenStringSanitizerQuietExitForAllHttpComponents()
689688

690689
Assert.Equal(0, matcher.CompareHeaderDictionaries(targetUntouchedEntry.Request.Headers, targetEntry.Request.Headers, new HashSet<string>(), new HashSet<string>()));
691690
Assert.Equal(0, matcher.CompareHeaderDictionaries(targetUntouchedEntry.Response.Headers, targetEntry.Response.Headers, new HashSet<string>(), new HashSet<string>()));
692-
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Request.Body, targetEntry.Request.Body));
693-
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Response.Body, targetEntry.Response.Body));
691+
692+
targetUntouchedEntry.Request.TryGetContentType(out var contentType);
693+
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Request.Body, targetEntry.Request.Body, contentType));
694+
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Response.Body, targetEntry.Response.Body, contentType));
694695
Assert.Equal(targetUntouchedEntry.RequestUri, targetEntry.RequestUri);
695696
}
696697

@@ -769,8 +770,9 @@ public async void BodyStringSanitizerQuietlyExits(string targetValue, string rep
769770
await session.Session.Sanitize(sanitizer);
770771

771772
var resultBodyValue = Encoding.UTF8.GetString(targetEntry.Request.Body);
772-
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Request.Body, targetEntry.Request.Body));
773-
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Response.Body, targetEntry.Response.Body));
773+
targetUntouchedEntry.Request.TryGetContentType(out var contentType);
774+
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Request.Body, targetEntry.Request.Body, contentType));
775+
Assert.Equal(0, matcher.CompareBodies(targetUntouchedEntry.Response.Body, targetEntry.Response.Body, contentType));
774776
}
775777

776778
[Fact]
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq.Expressions;
4+
using System.Text;
5+
using System.Text.Json;
6+
7+
namespace Azure.Sdk.Tools.TestProxy.Common
8+
{
9+
public class JsonComparer
10+
{
11+
public static List<string> CompareJson(byte[] json1, byte[] json2)
12+
{
13+
var differences = new List<string>();
14+
JsonDocument doc1;
15+
JsonDocument doc2;
16+
17+
// Deserialize the byte arrays to JsonDocument
18+
try
19+
{
20+
doc1 = JsonDocument.Parse(json1);
21+
}
22+
catch(Exception ex)
23+
{
24+
differences.Add($"Unable to parse the request json body. Content \"{Encoding.UTF8.GetString(json1)}.\" Exception: {ex.Message}");
25+
return differences;
26+
}
27+
28+
try
29+
{
30+
doc2 = JsonDocument.Parse(json2);
31+
}
32+
33+
catch (Exception ex)
34+
{
35+
differences.Add($"Unable to parse the record json body. Content \"{Encoding.UTF8.GetString(json2)}.\" Exception: {ex.Message}");
36+
return differences;
37+
}
38+
39+
CompareElements(doc1.RootElement, doc2.RootElement, differences, "");
40+
41+
return differences;
42+
}
43+
44+
private static void CompareElements(JsonElement element1, JsonElement element2, List<string> differences, string path)
45+
{
46+
if (element1.ValueKind != element2.ValueKind)
47+
{
48+
differences.Add($"{path}: Request and record have different types.");
49+
return;
50+
}
51+
52+
switch (element1.ValueKind)
53+
{
54+
case JsonValueKind.Object:
55+
{
56+
var properties1 = element1.EnumerateObject();
57+
var properties2 = element2.EnumerateObject();
58+
59+
var propDict1 = new Dictionary<string, JsonElement>();
60+
var propDict2 = new Dictionary<string, JsonElement>();
61+
62+
foreach (var prop in properties1)
63+
propDict1[prop.Name] = prop.Value;
64+
65+
foreach (var prop in properties2)
66+
propDict2[prop.Name] = prop.Value;
67+
68+
foreach (var key in propDict1.Keys)
69+
{
70+
if (propDict2.ContainsKey(key))
71+
{
72+
CompareElements(propDict1[key], propDict2[key], differences, $"{path}.{key}");
73+
}
74+
else
75+
{
76+
differences.Add($"{path}.{key}: Missing in request JSON");
77+
}
78+
}
79+
80+
foreach (var key in propDict2.Keys)
81+
{
82+
if (!propDict1.ContainsKey(key))
83+
{
84+
differences.Add($"{path}.{key}: Missing in record JSON");
85+
}
86+
}
87+
88+
break;
89+
}
90+
case JsonValueKind.Array:
91+
{
92+
var array1 = element1.EnumerateArray();
93+
var array2 = element2.EnumerateArray();
94+
95+
int index = 0;
96+
var enum1 = array1.GetEnumerator();
97+
var enum2 = array2.GetEnumerator();
98+
99+
while (enum1.MoveNext() && enum2.MoveNext())
100+
{
101+
CompareElements(enum1.Current, enum2.Current, differences, $"{path}[{index}]");
102+
index++;
103+
}
104+
105+
while (enum1.MoveNext())
106+
{
107+
differences.Add($"{path}[{index}]: Extra element in request JSON");
108+
index++;
109+
}
110+
111+
while (enum2.MoveNext())
112+
{
113+
differences.Add($"{path}[{index}]: Extra element in record JSON");
114+
index++;
115+
}
116+
117+
break;
118+
}
119+
case JsonValueKind.String:
120+
{
121+
if (element1.GetString() != element2.GetString())
122+
{
123+
differences.Add($"{path}: \"{element1.GetString()}\" != \"{element2.GetString()}\"");
124+
}
125+
break;
126+
}
127+
case JsonValueKind.Number:
128+
{
129+
if (element1.GetDecimal() != element2.GetDecimal())
130+
{
131+
differences.Add($"{path}: {element1.GetDecimal()} != {element2.GetDecimal()}");
132+
}
133+
break;
134+
}
135+
case JsonValueKind.True:
136+
case JsonValueKind.False:
137+
{
138+
if (element1.GetBoolean() != element2.GetBoolean())
139+
{
140+
differences.Add($"{path}: {element1.GetBoolean()} != {element2.GetBoolean()}");
141+
}
142+
break;
143+
}
144+
case JsonValueKind.Null:
145+
{
146+
// Both are null, nothing to compare
147+
break;
148+
}
149+
default:
150+
{
151+
differences.Add($"{path}: Unhandled value kind {element1.ValueKind}");
152+
break;
153+
}
154+
}
155+
}
156+
}
157+
}

tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordMatcher.cs

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,10 @@ public virtual RecordEntry FindMatch(RecordEntry request, IList<RecordEntry> ent
112112
if (!entry.IsTrack1Recording)
113113
{
114114
score += CompareHeaderDictionaries(request.Request.Headers, entry.Request.Headers, IgnoredHeaders, ExcludeHeaders);
115-
score += CompareBodies(request.Request.Body, entry.Request.Body);
115+
116+
request.Request.TryGetContentType(out var contentType);
117+
118+
score += CompareBodies(request.Request.Body, entry.Request.Body, descriptionBuilder: null, contentType: contentType);
116119
}
117120

118121
if (score == 0)
@@ -130,7 +133,7 @@ public virtual RecordEntry FindMatch(RecordEntry request, IList<RecordEntry> ent
130133
throw new TestRecordingMismatchException(GenerateException(request, bestScoreEntry, entries));
131134
}
132135

133-
public virtual int CompareBodies(byte[] requestBody, byte[] recordBody, StringBuilder descriptionBuilder = null)
136+
public virtual int CompareBodies(byte[] requestBody, byte[] recordBody, string contentType, StringBuilder descriptionBuilder = null)
134137
{
135138
if (!_compareBodies)
136139
{
@@ -154,27 +157,50 @@ public virtual int CompareBodies(byte[] requestBody, byte[] recordBody, StringBu
154157
return 1;
155158
}
156159

160+
157161
if (!requestBody.SequenceEqual(recordBody))
158162
{
159-
if (descriptionBuilder != null)
163+
// we just failed sequence equality, before erroring, lets check if we're a json body and check for property equality
164+
if (!string.IsNullOrWhiteSpace(contentType) && contentType.Contains("json"))
160165
{
161-
var minLength = Math.Min(requestBody.Length, recordBody.Length);
162-
int i;
163-
for (i = 0; i < minLength - 1; i++)
166+
var jsonDifferences = JsonComparer.CompareJson(requestBody, recordBody);
167+
168+
if (jsonDifferences.Count > 0)
164169
{
165-
if (requestBody[i] != recordBody[i])
170+
171+
if (descriptionBuilder != null)
166172
{
167-
break;
173+
descriptionBuilder.AppendLine($"There are differences between request and recordentry bodies:");
174+
foreach (var jsonDifference in jsonDifferences)
175+
{
176+
descriptionBuilder.AppendLine(jsonDifference);
177+
}
168178
}
179+
180+
return 1;
169181
}
170-
descriptionBuilder.AppendLine($"Request and record bodies do not match at index {i}:");
171-
var before = Math.Max(0, i - 10);
172-
var afterRequest = Math.Min(i + 20, requestBody.Length);
173-
var afterResponse = Math.Min(i + 20, recordBody.Length);
174-
descriptionBuilder.AppendLine($" request: \"{Encoding.UTF8.GetString(requestBody, before, afterRequest - before)}\"");
175-
descriptionBuilder.AppendLine($" record: \"{Encoding.UTF8.GetString(recordBody, before, afterResponse - before)}\"");
182+
}
183+
else {
184+
if (descriptionBuilder != null)
185+
{
186+
var minLength = Math.Min(requestBody.Length, recordBody.Length);
187+
int i;
188+
for (i = 0; i < minLength - 1; i++)
189+
{
190+
if (requestBody[i] != recordBody[i])
191+
{
192+
break;
193+
}
194+
}
195+
descriptionBuilder.AppendLine($"Request and record bodies do not match at index {i}:");
196+
var before = Math.Max(0, i - 10);
197+
var afterRequest = Math.Min(i + 20, requestBody.Length);
198+
var afterResponse = Math.Min(i + 20, recordBody.Length);
199+
descriptionBuilder.AppendLine($" request: \"{Encoding.UTF8.GetString(requestBody, before, afterRequest - before)}\"");
200+
descriptionBuilder.AppendLine($" record: \"{Encoding.UTF8.GetString(recordBody, before, afterResponse - before)}\"");
201+
}
202+
return 1;
176203
}
177-
return 1;
178204
}
179205

180206
return 0;
@@ -250,7 +276,8 @@ private string GenerateException(RecordEntry request, RecordEntry bestScoreEntry
250276

251277
builder.AppendLine("Body differences:");
252278

253-
CompareBodies(request.Request.Body, bestScoreEntry.Request.Body, builder);
279+
request.Request.TryGetContentType(out var contentType);
280+
CompareBodies(request.Request.Body, bestScoreEntry.Request.Body, contentType, descriptionBuilder: builder);
254281

255282
return builder.ToString();
256283
}

tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordSession.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,10 +102,10 @@ public RecordEntry Lookup(RecordEntry requestEntry, RecordMatcher matcher, IEnum
102102
{
103103
sanitizer.Sanitize(requestEntry);
104104
}
105+
105106
// normalize request body with STJ using relaxed escaping to match behavior when Deserializing from session files
106107
RecordEntry.NormalizeJsonBody(requestEntry.Request);
107108

108-
109109
RecordEntry entry = matcher.FindMatch(requestEntry, Entries);
110110
if (remove)
111111
{

0 commit comments

Comments
 (0)