Skip to content

Commit

Permalink
Support not recording request body (#2692)
Browse files Browse the repository at this point in the history
Addresses #2434
  • Loading branch information
JoshLove-msft authored Feb 8, 2022
1 parent 5a9916d commit a9a28e6
Show file tree
Hide file tree
Showing 10 changed files with 221 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ public async void TestSetMatcherIndividualRecording()
{
RecordingHandler testRecordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
var httpContext = new DefaultHttpContext();
await testRecordingHandler.StartPlayback("Test.RecordEntries/oauth_request_with_variables.json", httpContext.Response);
await testRecordingHandler.StartPlaybackAsync("Test.RecordEntries/oauth_request_with_variables.json", httpContext.Response);
var recordingId = httpContext.Response.Headers["x-recording-id"];
httpContext.Request.Headers["x-recording-id"] = recordingId;
httpContext.Request.Headers["x-abstraction-identifier"] = "BodilessMatcher";
Expand Down Expand Up @@ -382,7 +382,7 @@ public async void TestAddSanitizerIndividualRecording()
{
RecordingHandler testRecordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
var httpContext = new DefaultHttpContext();
await testRecordingHandler.StartPlayback("Test.RecordEntries/oauth_request_with_variables.json", httpContext.Response);
await testRecordingHandler.StartPlaybackAsync("Test.RecordEntries/oauth_request_with_variables.json", httpContext.Response);
var recordingId = httpContext.Response.Headers["x-recording-id"];
httpContext.Request.Headers["x-recording-id"] = recordingId;
httpContext.Request.Headers["x-abstraction-identifier"] = "HeaderRegexSanitizer";
Expand Down Expand Up @@ -569,7 +569,7 @@ public async void TestAddTransformIndividualRecording()
{
RecordingHandler testRecordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
var httpContext = new DefaultHttpContext();
await testRecordingHandler.StartPlayback("Test.RecordEntries/oauth_request_with_variables.json", httpContext.Response);
await testRecordingHandler.StartPlaybackAsync("Test.RecordEntries/oauth_request_with_variables.json", httpContext.Response);
var recordingId = httpContext.Response.Headers["x-recording-id"];
var apiVersion = "2016-03-21";
httpContext.Request.Headers["x-api-version"] = apiVersion;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ public void RecordMatcherThrowsExceptionsWithDetails()
" <Some-Other-Header> is absent in record, value <V>" + Environment.NewLine +
" <Extra-Header> is absent in request, value <Extra-Value>" + Environment.NewLine +
"Body differences:" + Environment.NewLine +
"Request and response bodies do not match at index 40:" + Environment.NewLine +
"Request and record bodies do not match at index 40:" + Environment.NewLine +
" request: \"e and long.\"" + Environment.NewLine +
" record: \"e and long but it also doesn't\"" + Environment.NewLine,
exception.Message);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Primitives;
using Xunit;

namespace Azure.Sdk.Tools.TestProxy.Tests
Expand Down Expand Up @@ -190,7 +196,7 @@ public async Task TestInMemoryPurgesSucessfully()
var httpContext = new DefaultHttpContext();
var key = recordingHandler.InMemorySessions.Keys.First();

await recordingHandler.StartPlayback(key, httpContext.Response, Common.RecordingType.InMemory);
await recordingHandler.StartPlaybackAsync(key, httpContext.Response, Common.RecordingType.InMemory);
var playbackSession = httpContext.Response.Headers["x-recording-id"];
recordingHandler.StopPlayback(playbackSession, true);

Expand All @@ -204,7 +210,7 @@ public async Task TestInMemoryDoesntPurgeErroneously()
var httpContext = new DefaultHttpContext();
var key = recordingHandler.InMemorySessions.Keys.First();

await recordingHandler.StartPlayback(key, httpContext.Response, Common.RecordingType.InMemory);
await recordingHandler.StartPlaybackAsync(key, httpContext.Response, Common.RecordingType.InMemory);
var playbackSession = httpContext.Response.Headers["x-recording-id"];
recordingHandler.StopPlayback(playbackSession, false);

Expand All @@ -221,7 +227,7 @@ public async Task TestLoadOfAbsoluteRecording()

var recordingHandler = new RecordingHandler(tmpPath);

await recordingHandler.StartPlayback(pathToRecording, httpContext.Response);
await recordingHandler.StartPlaybackAsync(pathToRecording, httpContext.Response);

var playbackSession = recordingHandler.PlaybackSessions.First();
var entry = playbackSession.Value.Session.Entries.First();
Expand All @@ -237,7 +243,7 @@ public async Task TestLoadOfRelativeRecording()
var pathToRecording = "Test.RecordEntries/oauth_request.json";
var recordingHandler = new RecordingHandler(currentPath);

await recordingHandler.StartPlayback(pathToRecording, httpContext.Response);
await recordingHandler.StartPlaybackAsync(pathToRecording, httpContext.Response);

var playbackSession = recordingHandler.PlaybackSessions.First();
var entry = playbackSession.Value.Session.Entries.First();
Expand Down Expand Up @@ -292,6 +298,115 @@ public void TestWriteRelativeRecording()
}
}

[Fact]
public async Task TestCanSkipRecordingRequestBody()
{
var currentPath = Directory.GetCurrentDirectory();
var httpContext = new DefaultHttpContext();
var pathToRecording = "recordings/skip_body";
var recordingHandler = new RecordingHandler(currentPath);
var fullPathToRecording = Path.Combine(currentPath, pathToRecording) + ".json";

recordingHandler.StartRecording(pathToRecording, httpContext.Response);
var sessionId = httpContext.Response.Headers["x-recording-id"].ToString();

CreateRecordModeRequest(httpContext, "request-body");

var mockClient = new HttpClient(new MockHttpHandler());
await recordingHandler.HandleRecordRequestAsync(sessionId, httpContext.Request, httpContext.Response, mockClient);
recordingHandler.StopRecording(sessionId);

try
{
using var fileStream = File.Open(fullPathToRecording, FileMode.Open);
using var doc = JsonDocument.Parse(fileStream);
var record = RecordSession.Deserialize(doc.RootElement);
var entry = record.Entries.First();
Assert.Null(entry.Request.Body);
Assert.Equal(MockHttpHandler.DefaultResponse, Encoding.UTF8.GetString(entry.Response.Body));
}
finally
{
File.Delete(fullPathToRecording);
}
}

[Fact]
public async Task TestCanSkipRecordingEntireRequestResponse()
{
var currentPath = Directory.GetCurrentDirectory();
var httpContext = new DefaultHttpContext();
var pathToRecording = "recordings/skip_entry";
var recordingHandler = new RecordingHandler(currentPath);
var fullPathToRecording = Path.Combine(currentPath, pathToRecording) + ".json";

recordingHandler.StartRecording(pathToRecording, httpContext.Response);
var sessionId = httpContext.Response.Headers["x-recording-id"].ToString();

CreateRecordModeRequest(httpContext, "request-response");

var mockClient = new HttpClient(new MockHttpHandler());
await recordingHandler.HandleRecordRequestAsync(sessionId, httpContext.Request, httpContext.Response, mockClient);

httpContext = new DefaultHttpContext();
// send a second request that SHOULD be recorded
CreateRecordModeRequest(httpContext);
httpContext.Request.Headers.Remove("x-recording-skip");
httpContext.Request.Body = TestHelpers.GenerateStreamRequestBody("{ \"key\": \"value\" }");
await recordingHandler.HandleRecordRequestAsync(sessionId, httpContext.Request, httpContext.Response, mockClient);

recordingHandler.StopRecording(sessionId);

try
{
using var fileStream = File.Open(fullPathToRecording, FileMode.Open);
using var doc = JsonDocument.Parse(fileStream);
var record = RecordSession.Deserialize(doc.RootElement);
Assert.Single(record.Entries);
var entry = record.Entries.First();
Assert.Equal("value", JsonDocument.Parse(entry.Request.Body).RootElement.GetProperty("key").GetString());
Assert.Equal(MockHttpHandler.DefaultResponse, Encoding.UTF8.GetString(entry.Response.Body));
}
finally
{
File.Delete(fullPathToRecording);
}
}

[Theory]
[InlineData("invalid value")]
[InlineData("")]
[InlineData("request-body", "request-response")]
public async Task TestInvalidRecordModeThrows(params string[] values)
{
var currentPath = Directory.GetCurrentDirectory();
var httpContext = new DefaultHttpContext();
var pathToRecording = "recordings/invalid_record_mode";
var recordingHandler = new RecordingHandler(currentPath);

recordingHandler.StartRecording(pathToRecording, httpContext.Response);
var sessionId = httpContext.Response.Headers["x-recording-id"].ToString();

CreateRecordModeRequest(httpContext, new StringValues(values));

var mockClient = new HttpClient(new MockHttpHandler());
HttpException resultingException = await Assert.ThrowsAsync<HttpException>(
async () => await recordingHandler.HandleRecordRequestAsync(sessionId, httpContext.Request, httpContext.Response, mockClient)
);
Assert.Equal(HttpStatusCode.BadRequest, resultingException.StatusCode);
}

private static void CreateRecordModeRequest(DefaultHttpContext context, StringValues mode = default)
{
context.Request.Headers["x-recording-skip"] = mode;
context.Request.Headers["x-recording-upstream-base-uri"] = "https://contoso.net";
context.Request.ContentType = "application/json";
context.Request.Method = "PUT";
context.Request.Body = TestHelpers.GenerateStreamRequestBody("{ \"key\": \"value\" }");
// content length must be set for the body to be parsed in SetMatcher
context.Request.ContentLength = context.Request.Body.Length;
}

[Fact]
public async Task TestLoadNonexistentAbsoluteRecording()
{
Expand All @@ -304,7 +419,7 @@ public async Task TestLoadNonexistentAbsoluteRecording()
var recordingHandler = new RecordingHandler(tmpPath);

var resultingException = await Assert.ThrowsAsync<TestRecordingMismatchException>(
async () => await recordingHandler.StartPlayback(pathToRecording, httpContext.Response)
async () => await recordingHandler.StartPlaybackAsync(pathToRecording, httpContext.Response)
);
Assert.Contains($"{recordingPath} does not exist", resultingException.Message);
}
Expand All @@ -319,7 +434,7 @@ public async Task TestLoadNonexistentRelativeRecording()
var recordingHandler = new RecordingHandler(currentPath);

var resultingException = await Assert.ThrowsAsync<TestRecordingMismatchException>(
async () => await recordingHandler.StartPlayback(pathToRecording, httpContext.Response)
async () => await recordingHandler.StartPlaybackAsync(pathToRecording, httpContext.Response)
);
Assert.Contains($"{pathToRecording} does not exist", resultingException.Message);
}
Expand Down Expand Up @@ -392,7 +507,7 @@ public async Task TestStartPlaybackWithVariables()
var recordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
httpContext.Response.Body = new MemoryStream();

await recordingHandler.StartPlayback("Test.RecordEntries/oauth_request_with_variables.json", httpContext.Response);
await recordingHandler.StartPlaybackAsync("Test.RecordEntries/oauth_request_with_variables.json", httpContext.Response);

Dictionary<string, string> results = JsonConvert.DeserializeObject<Dictionary<string, string>>(
TestHelpers.GenerateStringFromStream(httpContext.Response.Body)
Expand All @@ -409,7 +524,7 @@ public async Task TestStartPlaybackWithoutVariables()
var startHttpContext = new DefaultHttpContext();
var recordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());

await recordingHandler.StartPlayback("Test.RecordEntries/oauth_request.json", startHttpContext.Response);
await recordingHandler.StartPlaybackAsync("Test.RecordEntries/oauth_request.json", startHttpContext.Response);
}

[Fact]
Expand All @@ -425,4 +540,20 @@ public async Task CreateEntryUsesAbsoluteUri()
Assert.Equal(uri.AbsoluteUri, entry.RequestUri);
}
}

internal class MockHttpHandler : HttpMessageHandler
{
public const string DefaultResponse = "default response";

public MockHttpHandler()
{
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(DefaultResponse)
});
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

namespace Azure.Sdk.Tools.TestProxy.Common
{
public enum EntryRecordModel
public enum EntryRecordMode
{
Record,
DontRecord,
Expand Down
22 changes: 11 additions & 11 deletions tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/RecordMatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,49 +129,49 @@ public virtual RecordEntry FindMatch(RecordEntry request, IList<RecordEntry> ent
throw new TestRecordingMismatchException(GenerateException(request, bestScoreEntry));
}

public virtual int CompareBodies(byte[] requestBody, byte[] responseBody, StringBuilder descriptionBuilder = null)
public virtual int CompareBodies(byte[] requestBody, byte[] recordBody, StringBuilder descriptionBuilder = null)
{
if (!_compareBodies)
{
return 0;
}

if (requestBody == null && responseBody == null)
if (requestBody == null && recordBody == null)
{
return 0;
}

if (requestBody == null)
{
descriptionBuilder?.AppendLine("Request has body but response doesn't");
descriptionBuilder?.AppendLine("Record has body but request doesn't");
return 1;
}

if (responseBody == null)
if (recordBody == null)
{
descriptionBuilder?.AppendLine("Response has body but request doesn't");
descriptionBuilder?.AppendLine("Request has body but record doesn't");
return 1;
}

if (!requestBody.SequenceEqual(responseBody))
if (!requestBody.SequenceEqual(recordBody))
{
if (descriptionBuilder != null)
{
var minLength = Math.Min(requestBody.Length, responseBody.Length);
var minLength = Math.Min(requestBody.Length, recordBody.Length);
int i;
for (i = 0; i < minLength - 1; i++)
{
if (requestBody[i] != responseBody[i])
if (requestBody[i] != recordBody[i])
{
break;
}
}
descriptionBuilder.AppendLine($"Request and response bodies do not match at index {i}:");
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, responseBody.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(responseBody, before, afterResponse - before)}\"");
descriptionBuilder.AppendLine($" record: \"{Encoding.UTF8.GetString(recordBody, before, afterResponse - before)}\"");
}
return 1;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@ public class RecordTransport : HttpPipelineTransport
{
private readonly HttpPipelineTransport _innerTransport;

private readonly Func<RecordEntry, EntryRecordModel> _filter;
private readonly Func<RecordEntry, EntryRecordMode> _filter;

private readonly Random _random;

private readonly RecordSession _session;

public RecordTransport(RecordSession session, HttpPipelineTransport innerTransport, Func<RecordEntry, EntryRecordModel> filter, Random random)
public RecordTransport(RecordSession session, HttpPipelineTransport innerTransport, Func<RecordEntry, EntryRecordMode> filter, Random random)
{
_innerTransport = innerTransport;
_filter = filter;
Expand All @@ -50,10 +50,10 @@ private void Record(HttpMessage message)

switch (_filter(recordEntry))
{
case EntryRecordModel.Record:
case EntryRecordMode.Record:
_session.Record(recordEntry);
break;
case EntryRecordModel.RecordWithoutRequestBody:
case EntryRecordMode.RecordWithoutRequestBody:
recordEntry.Request.Body = null;
_session.Record(recordEntry);
break;
Expand Down
4 changes: 2 additions & 2 deletions tools/test-proxy/Azure.Sdk.Tools.TestProxy/Playback.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ public async Task Start()

if (String.IsNullOrEmpty(file) && !String.IsNullOrEmpty(recordingId))
{
await _recordingHandler.StartPlayback(recordingId, Response, RecordingType.InMemory);
await _recordingHandler.StartPlaybackAsync(recordingId, Response, RecordingType.InMemory);
}
else if(!String.IsNullOrEmpty(file))
{
await _recordingHandler.StartPlayback(file, Response, RecordingType.FilePersisted);
await _recordingHandler.StartPlaybackAsync(file, Response, RecordingType.FilePersisted);
}
else
{
Expand Down
Loading

0 comments on commit a9a28e6

Please sign in to comment.