Skip to content

Commit

Permalink
Info/Active/ now offers data for individual sessions (#5384)
Browse files Browse the repository at this point in the history
* Info controller now honors /Info/Active?id=<blah>
* new functionality tested in InfoTests.cs
* update RunTimeMetadataModel to omit fields from the 'base' configs. EG: ResponseTransform, RecordMatcher, RecordedTestSanitizer. Resulting in much cleaner output in /Info/Active and /Info/Available
  • Loading branch information
scbedd authored Feb 15, 2023
1 parent a967b97 commit 76afc9b
Show file tree
Hide file tree
Showing 9 changed files with 228 additions and 47 deletions.
35 changes: 34 additions & 1 deletion tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/InfoTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using Azure.Sdk.Tools.TestProxy.Common;
using Azure.Sdk.Tools.TestProxy.Common;
using Azure.Sdk.Tools.TestProxy.Matchers;
using Azure.Sdk.Tools.TestProxy.Models;
using Azure.Sdk.Tools.TestProxy.Sanitizers;
using Azure.Sdk.Tools.TestProxy.Transforms;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
Expand Down Expand Up @@ -58,5 +60,36 @@ public void TestReflectionModelWithAdvancedType()

var result = controller.Active();
}

[Fact]
public async Task TestReflectionModelWithTargetRecordSession()
{
RecordingHandler testRecordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
var httpContext = new DefaultHttpContext();

await testRecordingHandler.StartPlaybackAsync("Test.RecordEntries/multipart_request.json", httpContext.Response);
testRecordingHandler.Transforms.Clear();

var recordingId = httpContext.Response.Headers["x-recording-id"].ToString();

testRecordingHandler.AddSanitizerToRecording(recordingId, new UriRegexSanitizer(regex: "ABC123"));
testRecordingHandler.AddSanitizerToRecording(recordingId, new BodyRegexSanitizer(regex: ".+?"));
testRecordingHandler.SetMatcherForRecording(recordingId, new CustomDefaultMatcher(compareBodies: false, excludedHeaders: "an-excluded-header"));

var model = new ActiveMetadataModel(testRecordingHandler, recordingId);
var descriptions = model.Descriptions.ToList();

// we should have exactly 6 if we're counting all the customizations appropriately
Assert.True(descriptions.Count == 6);
Assert.True(model.Matchers.Count() == 1);
Assert.True(model.Sanitizers.Count() == 5);

// confirm that the overridden matcher is showing up
Assert.True(descriptions[3].ConstructorDetails.Arguments[1].Item2 == "\"ABC123\"");
Assert.True(descriptions[4].ConstructorDetails.Arguments[1].Item2 == "\".+?\"");

// and finally confirm our sanitizers are what we expect
Assert.True(descriptions[5].Name == "CustomDefaultMatcher");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;
namespace Azure.Sdk.Tools.TestProxy.Common.Exceptions
{
public class SessionNotActiveException: Exception
{
public SessionNotActiveException()
{
}
public SessionNotActiveException(string message)
: base(message)
{
}
}
}
22 changes: 17 additions & 5 deletions tools/test-proxy/Azure.Sdk.Tools.TestProxy/Info.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Azure.Sdk.Tools.TestProxy.Common.Exceptions;
using Azure.Sdk.Tools.TestProxy.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
Expand Down Expand Up @@ -33,15 +34,26 @@ public async Task<ContentResult> Available()
}

[HttpGet]
public async Task<ContentResult> Active()
public async Task<ContentResult> Active(string id="")
{
var dataModel = new ActiveMetadataModel(_recordingHandler);
var viewHtml = await RenderViewAsync(this, "ActiveExtensions", dataModel);
string content = string.Empty;

try
{
var dataModel = new ActiveMetadataModel(_recordingHandler, recordingId: id);
content = await RenderViewAsync(this, "ActiveExtensions", dataModel);
}
// if a SessionNotActiveException is thrown, we have passed in an invalid recordingId, otherwise it'll be an unhandled
// exception, which the exception middleware should surface just fine.
catch (SessionNotActiveException)
{
content = await RenderViewAsync(this, "Error", new ActiveMetadataModel(id));
}

return new ContentResult
{
ContentType = "text/html",
Content = viewHtml
Content = content
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,79 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Xml;
using System.IO;
using Azure.Sdk.Tools.TestProxy.Common;
using System.Collections.Concurrent;
using Microsoft.CodeAnalysis.Operations;
using Azure.Sdk.Tools.TestProxy.Common.Exceptions;

namespace Azure.Sdk.Tools.TestProxy.Models
{
public class ActiveMetadataModel : RunTimeMetaDataModel
{
public ActiveMetadataModel(string recordingId)
{
RecordingId = recordingId;
}

public ActiveMetadataModel(RecordingHandler pageRecordingHandler)
{
Descriptions = _populateFromHandler(pageRecordingHandler);
Descriptions = _populateFromHandler(pageRecordingHandler, "");
}

private List<ActionDescription> _populateFromHandler(RecordingHandler handler)
public ActiveMetadataModel(RecordingHandler pageRecordingHandler, string recordingId)
{
RecordingId = recordingId;
Descriptions = _populateFromHandler(pageRecordingHandler, recordingId);
}

public string RecordingId { get; set; }

private List<ActionDescription> _populateFromHandler(RecordingHandler handler, string recordingId)
{
var sanitizers = (IEnumerable<RecordedTestSanitizer>) handler.Sanitizers;
var transforms = (IEnumerable<ResponseTransform>) handler.Transforms;
var matcher = handler.Matcher;

List<ConcurrentDictionary<string, ModifiableRecordSession>> searchCollections = new List<ConcurrentDictionary<string, ModifiableRecordSession>>()
{
handler.PlaybackSessions,
handler.RecordingSessions,
handler.InMemorySessions
};

var recordingFound = false;
if (!string.IsNullOrWhiteSpace(recordingId)){
foreach (var sessionDict in searchCollections)
{
if (sessionDict.TryGetValue(recordingId, out var session))
{
sanitizers = sanitizers.Concat(session.AdditionalSanitizers);
transforms = transforms.Concat(session.AdditionalTransforms);

if (session.CustomMatcher != null)
{
matcher = session.CustomMatcher;
}

recordingFound = true;
break;
}
}

if (!recordingFound)
{
throw new SessionNotActiveException($"{recordingId} is not found in any Playback, Recording, or In-Memory sessions.");
}
}

List<ActionDescription> descriptions = new List<ActionDescription>();
var docXML = GetDocCommentXML();

descriptions.AddRange(handler.Sanitizers.Select(x => new ActionDescription()
descriptions.AddRange(sanitizers.Select(x => new ActionDescription()
{
ActionType = MetaDataType.Sanitizer,
Name = x.GetType().Name,
Expand All @@ -39,9 +92,9 @@ private List<ActionDescription> _populateFromHandler(RecordingHandler handler)
descriptions.Add(new ActionDescription()
{
ActionType = MetaDataType.Matcher,
Name = handler.Matcher.GetType().Name,
ConstructorDetails = GetInstanceDetails(handler.Matcher),
Description = GetClassDocComment(handler.Matcher.GetType(), docXML)
Name = matcher.GetType().Name,
ConstructorDetails = GetInstanceDetails(matcher),
Description = GetClassDocComment(matcher.GetType(), docXML)
});

return descriptions;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,32 +76,39 @@ public CtorDescription GetInstanceDetails(object target)
var arguments = new List<Tuple<string, string>>();

var filteredFields = fields.Where(x => x.FieldType.Name == "String" || x.FieldType.Name == "ApplyCondition");
foreach (FieldInfo field in filteredFields)

// we only want to crawl the fields if it is an inherited type. customizations are not offered
// when looking at a base RecordMatcher, ResponseTransform, or RecordedTestSanitizer
// These 3 will have a basetype of Object
if (tType.BaseType != typeof(Object))
{
var prop = field.GetValue(target);
string propValue;
if(prop == null)
{
propValue = "This argument is unset or null.";
}
else
foreach (FieldInfo field in filteredFields)
{
if(field.FieldType.Name == "ApplyCondition")
var prop = field.GetValue(target);
string propValue;
if (prop == null)
{
propValue = prop.ToString();

if(propValue == null)
{
continue;
}
propValue = "This argument is unset or null.";
}
else
{
propValue = "\"" + prop.ToString() + "\"";
if (field.FieldType.Name == "ApplyCondition")
{
propValue = prop.ToString();

if (propValue == null)
{
continue;
}
}
else
{
propValue = "\"" + prop.ToString() + "\"";
}
}

arguments.Add(new Tuple<string, string>(field.Name, propValue));
}

arguments.Add(new Tuple<string, string>(field.Name, propValue));
}

return new CtorDescription()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,4 @@
{
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:5001/",
"sslPort": 44362
}
},
"profiles": {
"Azure.Sdk.Tools.TestProxy": {
"commandName": "Project",
Expand All @@ -16,5 +8,13 @@
},
"applicationUrl": "https://localhost:5001;http://localhost:5000"
}
},
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:5001/",
"sslPort": 44362
}
}
}
14 changes: 13 additions & 1 deletion tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
- [A note about where sanitizers apply](#a-note-about-where-sanitizers-apply)
- [For Sanitizers, Matchers, or Transforms in general](#for-sanitizers-matchers-or-transforms-in-general)
- [Viewing available/active Sanitizers, Matchers, and Transforms](#viewing-availableactive-sanitizers-matchers-and-transforms)
- [To see customizations on a specific recording](#to-see-customizations-on-a-specific-recording)
- [Resetting active Sanitizers, Matchers, and Transforms](#resetting-active-sanitizers-matchers-and-transforms)
- [Reset the session](#reset-the-session)
- [Reset for a specific recordingId](#reset-for-a-specific-recordingid)
Expand Down Expand Up @@ -454,10 +455,21 @@ Currently, the configured set of transforms/playback/sanitizers are NOT propogat
Launch the test-proxy through your chosen method, then visit:

- `<proxyUrl>/Info/Available` to see all available
- `<proxyUrl>/Info/Active` to see all currently active.
- `<proxyUrl>/Info/Active` to see all currently active for all sessions.

Note that the `constructor arguments` that are documented must be present (where documented as such) in the body of the POST sent to the Admin Interface.

#### To see customizations on a specific recording

This only works **while a session is available**. A specific session is only available _before_ it has been stopped. Once that happens it has been written to disk or evacuated from server memory.

Example flow

- Start playback for "hello_world.json"
- Receive a recordingId back
- Place a breakpoint **before** the part of your code that calls `/Record/Stop` or `Playback/Stop`.
- Visit the url `<proxyUrl>/Info/Active?id=<your-recording-id>`

### Resetting active Sanitizers, Matchers, and Transforms

Given that the test-proxy offers the ability to set up customizations for an entire session or a single recording, it also must provide the ability to **reset** these settings without entirely restarting the server.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
@using Azure.Sdk.Tools.TestProxy.Models;
@using Azure.Sdk.Tools.TestProxy.Models;
@model ActiveMetadataModel

@{
Expand All @@ -14,12 +14,33 @@
@await Html.PartialAsync("css.cshtml")
</head>
<body>
<h1>
Test-Proxy - Active Extensions
</h1>
<p>
The below extensions are currently configured.
</p>
@if (!string.IsNullOrWhiteSpace(Model.RecordingId))
{
<h1>
Active Extensions for @Model.RecordingId
</h1>
}
else
{
<h1>
Active Extensions for All Sessions
</h1>
}


@if (!string.IsNullOrWhiteSpace(Model.RecordingId))
{
<p>
The below extensions are configured for recording <b>@Model.RecordingId</b>.
</p>
}
else
{
<p>
The below extensions are currently configured for all sessions.
</p>
}

<p>
To observe ALL extensions (rather than just what is currently active), visit <a href="/Info/Available">/Info/Available</a>. For clarity, note that argument values below are surrounded in quotes. The actual value itself being utilized by
the test-proxy does not contain these quotes.
Expand Down
29 changes: 29 additions & 0 deletions tools/test-proxy/Azure.Sdk.Tools.TestProxy/Views/Info/Error.cshtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
@using Azure.Sdk.Tools.TestProxy.Models;
@model ActiveMetadataModel

@{
Layout = null;
}

<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>An error has occured.</title>

@await Html.PartialAsync("css.cshtml")
</head>
<body>
<h1>
Unable to locate @Model.RecordingId.
</h1>

<p>
Unfortunately, a recording id with value "@Model.RecordingId" can not be located in currently active Playback, Recording, or In-Memory sessions.
</p>

<p>
Please confirm that the recording-id in question has <b>NOT</b> been stopped yet. Once a record/playback session has ended, the customizations for that particular session can no longer be retrieved.
</p>
</body>
</html>

0 comments on commit 76afc9b

Please sign in to comment.