diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/InfoTests.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/InfoTests.cs index 2765ca24661..0e07ce1ae7c 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/InfoTests.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy.Tests/InfoTests.cs @@ -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; @@ -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"); + } } } diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/Exceptions/SessionNotActiveException.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/Exceptions/SessionNotActiveException.cs new file mode 100644 index 00000000000..8a9da5a1744 --- /dev/null +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Common/Exceptions/SessionNotActiveException.cs @@ -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) + { + } + } +} diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Info.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Info.cs index 21db1710443..686db9b6fbf 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Info.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Info.cs @@ -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; @@ -33,15 +34,26 @@ public async Task Available() } [HttpGet] - public async Task Active() + public async Task 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 }; } diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Models/ActiveMetadataModel.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Models/ActiveMetadataModel.cs index 3fc30b0f8ea..18511558726 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Models/ActiveMetadataModel.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Models/ActiveMetadataModel.cs @@ -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 _populateFromHandler(RecordingHandler handler) + public ActiveMetadataModel(RecordingHandler pageRecordingHandler, string recordingId) + { + RecordingId = recordingId; + Descriptions = _populateFromHandler(pageRecordingHandler, recordingId); + } + + public string RecordingId { get; set; } + + private List _populateFromHandler(RecordingHandler handler, string recordingId) { + var sanitizers = (IEnumerable) handler.Sanitizers; + var transforms = (IEnumerable) handler.Transforms; + var matcher = handler.Matcher; + + List> searchCollections = new List>() + { + 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 descriptions = new List(); 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, @@ -39,9 +92,9 @@ private List _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; diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Models/RunTimeMetadataModel.cs b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Models/RunTimeMetadataModel.cs index 59643ed9358..314a129e1c5 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Models/RunTimeMetadataModel.cs +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Models/RunTimeMetadataModel.cs @@ -76,32 +76,39 @@ public CtorDescription GetInstanceDetails(object target) var arguments = new List>(); 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(field.Name, propValue)); } - - arguments.Add(new Tuple(field.Name, propValue)); } return new CtorDescription() diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Properties/launchSettings.json b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Properties/launchSettings.json index 3bc93e560a3..77fea943f32 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Properties/launchSettings.json +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Properties/launchSettings.json @@ -1,12 +1,4 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:5001/", - "sslPort": 44362 - } - }, "profiles": { "Azure.Sdk.Tools.TestProxy": { "commandName": "Project", @@ -16,5 +8,13 @@ }, "applicationUrl": "https://localhost:5001;http://localhost:5000" } + }, + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:5001/", + "sslPort": 44362 + } } } \ No newline at end of file diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md index 21e8b6122f5..b8d8c995e0b 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/README.md @@ -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) @@ -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: - `/Info/Available` to see all available -- `/Info/Active` to see all currently active. +- `/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 `/Info/Active?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. diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Views/Info/ActiveExtensions.cshtml b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Views/Info/ActiveExtensions.cshtml index 8aed7c33f00..06535980803 100644 --- a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Views/Info/ActiveExtensions.cshtml +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Views/Info/ActiveExtensions.cshtml @@ -1,4 +1,4 @@ -@using Azure.Sdk.Tools.TestProxy.Models; +@using Azure.Sdk.Tools.TestProxy.Models; @model ActiveMetadataModel @{ @@ -14,12 +14,33 @@ @await Html.PartialAsync("css.cshtml") -

- Test-Proxy - Active Extensions -

-

- The below extensions are currently configured. -

+ @if (!string.IsNullOrWhiteSpace(Model.RecordingId)) + { +

+ Active Extensions for @Model.RecordingId +

+ } + else + { +

+ Active Extensions for All Sessions +

+ } + + + @if (!string.IsNullOrWhiteSpace(Model.RecordingId)) + { +

+ The below extensions are configured for recording @Model.RecordingId. +

+ } + else + { +

+ The below extensions are currently configured for all sessions. +

+ } +

To observe ALL extensions (rather than just what is currently active), visit /Info/Available. 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. diff --git a/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Views/Info/Error.cshtml b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Views/Info/Error.cshtml new file mode 100644 index 00000000000..7d1989d5f74 --- /dev/null +++ b/tools/test-proxy/Azure.Sdk.Tools.TestProxy/Views/Info/Error.cshtml @@ -0,0 +1,29 @@ +@using Azure.Sdk.Tools.TestProxy.Models; +@model ActiveMetadataModel + +@{ + Layout = null; +} + + + + + + An error has occured. + + @await Html.PartialAsync("css.cshtml") + + +

+ Unable to locate @Model.RecordingId. +

+ +

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

+ +

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

+ + \ No newline at end of file