Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d4f0636
adding recognition for the mode. adding a local test script so I can …
scbedd Sep 4, 2025
78883de
working through the various issues
scbedd Sep 4, 2025
ab33cd3
be able to hadnle record or playback, but I really need to solidify h…
scbedd Sep 9, 2025
b01efb1
handle absolute form URIs. time to eliminate last remaining bugs
scbedd Sep 10, 2025
85da442
getting much closer to this thing actually working. still need to fix…
scbedd Sep 10, 2025
655cc99
we have a successful standard proxy mode recording!
scbedd Sep 10, 2025
6072421
fix build error
scbedd Sep 10, 2025
ad01bca
update defaults
scbedd Sep 11, 2025
b543106
remove my default. update comments. address feedback. keep --http-pro…
scbedd Sep 12, 2025
33c75c9
ensure that HandleRequest gets the correct id
scbedd Sep 12, 2025
1434646
add some tests
scbedd Sep 12, 2025
db847cb
undo changes to launchSettings. ensure that playbacktests and recordt…
scbedd Sep 12, 2025
3116b48
ensure tests clean up after themselves to
scbedd Sep 12, 2025
bc0c918
simplify tests and fix for collection access
scbedd Sep 15, 2025
f2e488f
simplify some tests
scbedd Sep 15, 2025
3672764
ensure these are actually disposable and run their teardown properly
scbedd Sep 15, 2025
03476dc
add a diagnosis step
scbedd Sep 15, 2025
e3917e8
conditions
scbedd Sep 15, 2025
7d30231
Merge branch 'main' into native-proxy-support
scbedd Sep 15, 2025
00f205a
additional reachability diagnosis
scbedd Sep 15, 2025
40b4960
remove a CLI setting
scbedd Sep 15, 2025
60239a4
add the reachability check before any install
scbedd Sep 15, 2025
4671ac1
trying setting network isolation to permissive
scbedd Sep 15, 2025
ffef397
re-align archetype-sdk-tools-dotnet to align with original configurat…
scbedd Sep 15, 2025
6bebcee
swapping permissiveness to only nuget.org
scbedd Sep 16, 2025
7f2b5ee
Revert "swapping permissiveness to only nuget.org"
scbedd Sep 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion eng/pipelines/templates/stages/1es-redirect.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ extends:
parameters:
settings:
skipBuildTagsForGitHubPullRequests: true
networkIsolationPolicy: Permissive
sdl:
# Turn off the build warnings caused by disabling some sdl checks
createAdoIssuesForJustificationsForDisablement: false
Expand All @@ -52,7 +53,7 @@ extends:
justificationForDisabling: CodeQL times our pipelines out by running for 2+ hours before being force canceled.
componentgovernance:
enabled: false
justificationForDisabling: Manually enabling only on the main build job instead of running it on every job.
justificationForDisabling: Manually enabling only on the main build job instead of running it on every job.
psscriptanalyzer:
compiled: true
break: true
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
using Xunit;

[CollectionDefinition("AccessesProxyConfig", DisableParallelization = true)]
public class AccessesProxyConfigCollection : ICollectionFixture<AccessesProxyConfigResetFixture> { }

public sealed class AccessesProxyConfigResetFixture { }
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
using Azure.Sdk.Tools.TestProxy.Common.Exceptions;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Azure.Sdk.Tools.TestProxy.Common;
using Xunit;
using System.Text.Json;
using Azure.Sdk.Tools.TestProxy.Common.Exceptions;
using Azure.Sdk.Tools.TestProxy.Store;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;

namespace Azure.Sdk.Tools.TestProxy.Tests
{
public class PlaybackTests
{

private NullLoggerFactory _nullLogger = new NullLoggerFactory();

[Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using Xunit;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
using System;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Azure.Sdk.Tools.TestProxy.Common;
using Azure.Sdk.Tools.TestProxy.Common.Exceptions;
using Azure.Sdk.Tools.TestProxy.Models;
using Azure.Sdk.Tools.TestProxy.Store;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using Xunit;

namespace Azure.Sdk.Tools.TestProxy.Tests
{
[Collection("AccessesProxyConfig")]
public class StandardPlaybackTests : IDisposable
{
public StandardPlaybackTests()
{
Startup.ProxyConfiguration.Mode = UniversalRecordingMode.Azure;
Startup.ProxyConfiguration.RecordingId = null;
}

public void Dispose()
{
Startup.ProxyConfiguration.Mode = UniversalRecordingMode.Azure;
Startup.ProxyConfiguration.RecordingId = null;
}

[Fact]
public async Task TestPlaybackStartStandardProxy()
{
// set the startup mode to standard record
Startup.ProxyConfiguration.Mode = UniversalRecordingMode.StandardRecord;
Startup.ProxyConfiguration.RecordingId = string.Empty;
RecordingHandler testRecordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
var httpContext = new DefaultHttpContext();
var body = "{\"x-recording-file\":\"Test.RecordEntries/failing_multipart_body.json\"}";
httpContext.Request.Body = TestHelpers.GenerateStreamRequestBody(body);
httpContext.Request.ContentLength = body.Length;
var controller = new Playback(testRecordingHandler, new NullLoggerFactory())
{
ControllerContext = new ControllerContext()
{
HttpContext = httpContext
}
};
await controller.Start();
var recordingId = httpContext.Response.Headers["x-recording-id"].ToString();
Assert.False(string.IsNullOrEmpty(recordingId));
Assert.True(testRecordingHandler.PlaybackSessions.ContainsKey(recordingId));
Assert.Equal(UniversalRecordingMode.StandardPlayback, Startup.ProxyConfiguration.Mode);
Assert.Equal(recordingId, Startup.ProxyConfiguration.RecordingId);
}

[Fact]
public async Task TestStopPlaybackStandardProxy()
{
Startup.ProxyConfiguration.Mode = UniversalRecordingMode.StandardRecord;
Startup.ProxyConfiguration.RecordingId = string.Empty;
RecordingHandler testRecordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
var httpContext = new DefaultHttpContext();
var body = "{\"x-recording-file\":\"Test.RecordEntries/failing_multipart_body.json\"}";
httpContext.Request.Body = TestHelpers.GenerateStreamRequestBody(body);
httpContext.Request.ContentLength = body.Length;
var controller = new Playback(testRecordingHandler, new NullLoggerFactory())
{
ControllerContext = new ControllerContext()
{
HttpContext = httpContext
}
};
await controller.Start();
await controller.Stop();
Assert.Null(Startup.ProxyConfiguration.RecordingId);
Assert.Equal(UniversalRecordingMode.StandardPlayback, Startup.ProxyConfiguration.Mode);
}

[Fact]
public async Task TestTopPlaybackThrowsOnWrongMode()
{
Startup.ProxyConfiguration.Mode = UniversalRecordingMode.StandardRecord;
Startup.ProxyConfiguration.RecordingId = string.Empty;
RecordingHandler testRecordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
var httpContext = new DefaultHttpContext();
var body = "{\"x-recording-file\":\"Test.RecordEntries/failing_multipart_body.json\"}";
httpContext.Request.Body = TestHelpers.GenerateStreamRequestBody(body);
httpContext.Request.ContentLength = body.Length;
var controller = new Playback(testRecordingHandler, new NullLoggerFactory())
{
ControllerContext = new ControllerContext()
{
HttpContext = httpContext
}
};
await controller.Start();

Startup.ProxyConfiguration.Mode = UniversalRecordingMode.StandardRecord;
await Assert.ThrowsAsync<HttpException>(
() => controller.Stop()
);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
using Azure.Sdk.Tools.TestProxy.Common.Exceptions;
using Azure.Sdk.Tools.TestProxy.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging.Abstractions;
using System;
using System.IO;
using System.Threading.Tasks;
using Xunit;

namespace Azure.Sdk.Tools.TestProxy.Tests
{
[Collection("AccessesProxyConfig")]
public class StandardRecordTests : IDisposable
{
public StandardRecordTests()
{
Startup.ProxyConfiguration.Mode = UniversalRecordingMode.Azure;
Startup.ProxyConfiguration.RecordingId = null;
}

public void Dispose()
{
Startup.ProxyConfiguration.Mode = UniversalRecordingMode.Azure;
Startup.ProxyConfiguration.RecordingId = null;
}

[Fact]
public async Task TestStartRecordStandardProxy()
{
Startup.ProxyConfiguration.Mode = UniversalRecordingMode.StandardRecord;
Startup.ProxyConfiguration.RecordingId = string.Empty;
RecordingHandler testRecordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
var httpContext = new DefaultHttpContext();
var body = "{\"x-recording-file\":\"TestStartRecordNewFormat.json\"}";
httpContext.Request.Body = TestHelpers.GenerateStreamRequestBody(body);
httpContext.Request.ContentLength = body.Length;
var controller = new Record(testRecordingHandler, new NullLoggerFactory())
{
ControllerContext = new ControllerContext()
{
HttpContext = httpContext
}
};
await controller.Start();
var recordingId = httpContext.Response.Headers["x-recording-id"].ToString();
Assert.False(string.IsNullOrEmpty(recordingId));
Assert.True(testRecordingHandler.RecordingSessions.ContainsKey(recordingId));
Assert.Equal(UniversalRecordingMode.StandardRecord, Startup.ProxyConfiguration.Mode);
Assert.Equal(recordingId, Startup.ProxyConfiguration.RecordingId);
}

[Fact]
public async Task TestStopRecordStandardProxy()
{
var testFile = "TestStartRecordNewFormat.json";
Startup.ProxyConfiguration.Mode = UniversalRecordingMode.StandardRecord;
Startup.ProxyConfiguration.RecordingId = string.Empty;
RecordingHandler testRecordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
var httpContext = new DefaultHttpContext();
var body = "{\"x-recording-file\":\"" + testFile + "\"}";
httpContext.Request.Body = TestHelpers.GenerateStreamRequestBody(body);
httpContext.Request.ContentLength = body.Length;
var controller = new Record(testRecordingHandler, new NullLoggerFactory())
{
ControllerContext = new ControllerContext()
{
HttpContext = httpContext
}
};
await controller.Start();
httpContext.Request.ContentLength = 0;
await controller.Stop();
Assert.True(File.Exists(Path.Combine(Directory.GetCurrentDirectory(), testFile)));
}

[Fact]
public async Task TestTopRecordThrowsOnWrongMode()
{
var testFile = "TestStartRecordNewFormat.json";
Startup.ProxyConfiguration.Mode = UniversalRecordingMode.StandardRecord;
Startup.ProxyConfiguration.RecordingId = string.Empty;
RecordingHandler testRecordingHandler = new RecordingHandler(Directory.GetCurrentDirectory());
var httpContext = new DefaultHttpContext();
var body = "{\"x-recording-file\":\"" + testFile + "\"}";
httpContext.Request.Body = TestHelpers.GenerateStreamRequestBody(body);
httpContext.Request.ContentLength = body.Length;
var controller = new Record(testRecordingHandler, new NullLoggerFactory())
{
ControllerContext = new ControllerContext()
{
HttpContext = httpContext
}
};
await controller.Start();
httpContext.Request.ContentLength = 0;

Startup.ProxyConfiguration.Mode = UniversalRecordingMode.StandardPlayback;
await Assert.ThrowsAsync<HttpException>(
() => controller.Stop()
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ public static RootCommand GenerateCommandLineOptions(Func<DefaultOptions, Task>
getDefaultValue: () => false);
dumpOption.AddAlias("-d");

var standardProxyOption = new Option<bool>(
name: "--http-proxy",
description: "Flag; Run the test-proxy in single-session mode. This means that test-proxy will function as a regular proxy without needing any request header changes. Single playback or record at a time.",
getDefaultValue: () => false);

var universalOption = new Option<bool>(
name: "--universal",
description: "Flag; Redirect all logs to stdout, including what would normally be showing up on stderr.",
Expand Down Expand Up @@ -94,10 +99,11 @@ public static RootCommand GenerateCommandLineOptions(Func<DefaultOptions, Task>
startCommand.AddOption(dumpOption);
startCommand.AddOption(universalOption);
startCommand.AddOption(autoShutdownOption);
startCommand.AddOption(standardProxyOption);
startCommand.AddArgument(collectedArgs);

startCommand.SetHandler(async (startOpts) => await callback(startOpts),
new StartOptionsBinder(storageLocationOption, storagePluginOption, insecureOption, dumpOption, universalOption, autoShutdownOption, collectedArgs)
new StartOptionsBinder(storageLocationOption, storagePluginOption, insecureOption, dumpOption, universalOption, autoShutdownOption, standardProxyOption, collectedArgs)
);
root.Add(startCommand);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ public class StartOptions : DefaultOptions

public int AutoShutdownTime { get; set; }

public bool StandardProxyMode { get; set; }

// On the command line, use -- and everything after that becomes arguments to Host.CreateDefaultBuilder
// For example Test-Proxy start -i -d -- --urls https://localhost:8002 would set AdditionaArgs to a list containing
// --urls and https://localhost:8002 as individual entries. This is converted to a string[] before being
Expand All @@ -26,16 +28,19 @@ public class StartOptionsBinder : BinderBase<StartOptions>
private readonly Option<bool> _dumpOption;
private readonly Option<bool> _univeralOutputOption;
private readonly Option<int> _autoShutdownTime;
private readonly Option<bool> _standardProxyMode;
private readonly Argument<string[]> _additionalArgs;


public StartOptionsBinder(Option<string> storageLocationOption, Option<string> storagePluginOption, Option<bool> insecureOption, Option<bool> dumpOption, Option<bool> universalOutput, Option<int> autoShutdownTime, Argument<string[]> additionalArgs)
public StartOptionsBinder(Option<string> storageLocationOption, Option<string> storagePluginOption, Option<bool> insecureOption, Option<bool> dumpOption, Option<bool> universalOutput, Option<int> autoShutdownTime, Option<bool> standardProxyMode, Argument<string[]> additionalArgs)
{
_storageLocationOption = storageLocationOption;
_storagePluginOption = storagePluginOption;
_insecureOption = insecureOption;
_dumpOption = dumpOption;
_univeralOutputOption = universalOutput;
_autoShutdownTime = autoShutdownTime;
_standardProxyMode = standardProxyMode;
_additionalArgs = additionalArgs;
}

Expand All @@ -48,6 +53,7 @@ protected override StartOptions GetBoundValue(BindingContext bindingContext) =>
Dump = bindingContext.ParseResult.GetValueForOption(_dumpOption),
UniversalOutput = bindingContext.ParseResult.GetValueForOption(_univeralOutputOption),
AutoShutdownTime = bindingContext.ParseResult.GetValueForOption(_autoShutdownTime),
StandardProxyMode = bindingContext.ParseResult.GetValueForOption(_standardProxyMode),
AdditionalArgs = bindingContext.ParseResult.GetValueForArgument(_additionalArgs)
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
namespace Azure.Sdk.Tools.TestProxy.Models
{
public enum UniversalRecordingMode
{
StandardRecord, // Requests that do not match /Admin should be directed to Playback.HandleRequest()
StandardPlayback, // Requests that do not match /Admin should be directed to Record.HandleRequest()
Azure // Azure mode, where direction to /Record/HandleRequest() or /Playback/HandleRequest() is determined by x-recording-mode header presence
}

public class ServerRecordingConfiguration
{
public UniversalRecordingMode Mode { get; set; } = UniversalRecordingMode.Azure;
public string RecordingId { get; set; }
}
}
Loading