Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions DevProxy.Abstractions/Proxy/ProxyEvents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ public class RecordingArgs(IEnumerable<RequestLog> requestLogs) : ProxyEventArgs
{
public IEnumerable<RequestLog> RequestLogs { get; set; } = requestLogs ??
throw new ArgumentNullException(nameof(requestLogs));
/// <summary>
/// Absolute paths of the report files written by reporters while
/// processing this recording.
/// </summary>
public ICollection<string> ReportFiles { get; } = [];
}

public class RequestLogArgs(RequestLog requestLog)
Expand Down
47 changes: 47 additions & 0 deletions DevProxy.Integration.Tests/JsonReporterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.

using DevProxy.Abstractions.Proxy;
using DevProxy.Abstractions.Utils;
using DevProxy.Plugins.Reporters;
using DevProxy.Plugins.Reporting;
using Microsoft.Extensions.Logging.Abstractions;
Expand All @@ -28,4 +29,50 @@ public void FileExtension_IsDeterminedPerReport()
_ = reporter.Report("OtherPlugin", new { Value = 1 });
Assert.Equal(".json", reporter.FileExtension);
}

[Fact]
public async Task AfterRecordingStop_RecordsAbsolutePathsOfWrittenReports()
{
var reporter = new TestJsonReporter();
var args = new RecordingArgs([])
{
GlobalData = new()
{
[ProxyUtils.ReportsKey] = new Dictionary<string, object>
{
["SomePlugin"] = new { Value = 1 },
["OtherPlugin"] = new { Value = 2 },
},
},
};

var dir = Directory.CreateTempSubdirectory("devproxy-reporter-");
var originalCwd = Directory.GetCurrentDirectory();
Directory.SetCurrentDirectory(dir.FullName);
try
{
await reporter.AfterRecordingStopAsync(args, CancellationToken.None);
}
finally
{
Directory.SetCurrentDirectory(originalCwd);
}

try
{
var reportFiles = args.ReportFiles;
Assert.Equal(2, reportFiles.Count);
Assert.All(reportFiles, f =>
{
Assert.True(Path.IsPathFullyQualified(f));
Assert.True(File.Exists(f));
});
Assert.Contains(reportFiles, f => Path.GetFileName(f) == "SomePlugin_JsonReporter.json");
Assert.Contains(reportFiles, f => Path.GetFileName(f) == "OtherPlugin_JsonReporter.json");
}
finally
{
dir.Delete(true);
}
}
}
1 change: 1 addition & 0 deletions DevProxy.Plugins/Reporters/BaseReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ value is not Dictionary<string, object> reports ||

Logger.LogInformation("Writing report {ReportKey} to {FileName}...", report.Key, fileName);
await File.WriteAllTextAsync(fileName, reportContents, cancellationToken);
e.ReportFiles.Add(Path.GetFullPath(fileName));
}

Logger.LogTrace("Left {Name}", nameof(AfterRecordingStopAsync));
Expand Down
6 changes: 4 additions & 2 deletions DevProxy.Tests/Fakes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ internal sealed class FakeProxyStateController : IProxyStateController

public void StartRecording() => StartRecordingCalls++;

public Task StopRecordingAsync(CancellationToken cancellationToken)
public Task<IReadOnlyList<string>> StopRecordingAsync(CancellationToken cancellationToken)
{
StopRecordingCalls++;
return Task.CompletedTask;
return Task.FromResult(ReportFiles);
}

public IReadOnlyList<string> ReportFiles { get; set; } = [];

public Task MockRequestAsync(CancellationToken cancellationToken)
{
MockRequestCalls++;
Expand Down
58 changes: 58 additions & 0 deletions DevProxy.Tests/ProxyControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using DevProxy.ApiControllers;
using Microsoft.AspNetCore.Mvc;
using Xunit;
Expand All @@ -26,4 +27,61 @@ public async Task RaiseMockRequestAsync_ReturnsNoContentAfterMockRequestComplete
_ = Assert.IsType<NoContentResult>(result);
Assert.Equal(1, stateController.MockRequestCalls);
}

private static ProxyController CreateController(FakeProxyStateController stateController, out X509Certificate2 cert)
{
using var rsa = RSA.Create(2048);
var request = new CertificateRequest("CN=test", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
cert = request.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(1));
return new ProxyController(stateController, new FakeProxyConfiguration(), cert);
}

[Fact]
public async Task SetAsync_StopRecording_ReturnsReportFiles()
{
var stateController = new FakeProxyStateController
{
ReportFiles = ["/tmp/ExecutionSummaryPlugin_JsonReporter.json"]
};
var controller = CreateController(stateController, out var cert);
using (cert)
{
var result = await controller.SetAsync(new ProxyInfo { Recording = false }, CancellationToken.None);

var ok = Assert.IsType<OkObjectResult>(result);
var info = Assert.IsType<ProxyInfo>(ok.Value);
Assert.Equal(1, stateController.StopRecordingCalls);
Assert.Equal(["/tmp/ExecutionSummaryPlugin_JsonReporter.json"], info.Reports);
}
}

[Fact]
public async Task SetAsync_StartRecording_OmitsReports()
{
var stateController = new FakeProxyStateController();
var controller = CreateController(stateController, out var cert);
using (cert)
{
var result = await controller.SetAsync(new ProxyInfo { Recording = true }, CancellationToken.None);

var ok = Assert.IsType<OkObjectResult>(result);
var info = Assert.IsType<ProxyInfo>(ok.Value);
Assert.Null(info.Reports);
Assert.DoesNotContain("reports", JsonSerializer.Serialize(info, JsonSerializerOptions.Web), StringComparison.Ordinal);
}
}

[Fact]
public async Task SetAsync_WithReports_ReturnsValidationProblem()
{
var stateController = new FakeProxyStateController();
var controller = CreateController(stateController, out var cert);
using (cert)
{
var result = await controller.SetAsync(new ProxyInfo { Recording = false, Reports = ["x"] }, CancellationToken.None);

_ = Assert.IsAssignableFrom<ObjectResult>(result);
Assert.Equal(0, stateController.StopRecordingCalls);
}
}
}
13 changes: 11 additions & 2 deletions DevProxy/ApiControllers/ProxyController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ public async Task<IActionResult> SetAsync([FromBody] ProxyInfo proxyInfo, Cancel
return ValidationProblem(ModelState);
}

if (proxyInfo.Reports != null)
{
ModelState.AddModelError("Reports", "Reports cannot be set");
return ValidationProblem(ModelState);
}

IReadOnlyList<string>? reports = null;
if (proxyInfo.Recording.HasValue)
{
if (proxyInfo.Recording.Value)
Expand All @@ -49,11 +56,13 @@ public async Task<IActionResult> SetAsync([FromBody] ProxyInfo proxyInfo, Cancel
}
else
{
await _proxyStateController.StopRecordingAsync(cancellationToken);
reports = await _proxyStateController.StopRecordingAsync(cancellationToken);
}
}

return Ok(ProxyInfo.From(_proxyStateController.ProxyState, _proxyConfiguration));
var result = ProxyInfo.From(_proxyStateController.ProxyState, _proxyConfiguration);
result.Reports = reports;
return Ok(result);
}

[HttpPost("mockRequest")]
Expand Down
7 changes: 7 additions & 0 deletions DevProxy/ApiControllers/ProxyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

using DevProxy.Abstractions.Proxy;
using DevProxy.Proxy;
using System.Text.Json.Serialization;

namespace DevProxy.ApiControllers;

Expand All @@ -13,6 +14,12 @@ public sealed class ProxyInfo
{
public bool? Recording { get; set; }
public string? ConfigFile { get; init; }
/// <summary>
/// Absolute paths of the report files written after recording stopped.
/// Only returned in response to stopping recording.
/// </summary>
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyList<string>? Reports { get; set; }

public static ProxyInfo From(IProxyState proxyState, IProxyConfiguration proxyConfiguration)
{
Expand Down
6 changes: 5 additions & 1 deletion DevProxy/Proxy/IProxyStateController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ public interface IProxyStateController
{
IProxyState ProxyState { get; }
void StartRecording();
Task StopRecordingAsync(CancellationToken cancellationToken);
/// <summary>
/// Stops recording and waits for plugins to process the recorded requests.
/// </summary>
/// <returns>Absolute paths of the report files written by reporters.</returns>
Task<IReadOnlyList<string>> StopRecordingAsync(CancellationToken cancellationToken);
Task MockRequestAsync(CancellationToken cancellationToken);
void StopProxy();
}
6 changes: 4 additions & 2 deletions DevProxy/Proxy/ProxyStateController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ public void StartRecording()
PrintRecordingIndicator(ProxyState.IsRecording);
}

public async Task StopRecordingAsync(CancellationToken cancellationToken)
public async Task<IReadOnlyList<string>> StopRecordingAsync(CancellationToken cancellationToken)
{
if (!ProxyState.IsRecording)
{
return;
return [];
}

ProxyState.IsRecording = false;
Expand All @@ -65,6 +65,8 @@ public async Task StopRecordingAsync(CancellationToken cancellationToken)
}
}
_logger.LogInformation("DONE");

return [.. recordingArgs.ReportFiles];
}

public async Task MockRequestAsync(CancellationToken cancellationToken)
Expand Down
5 changes: 3 additions & 2 deletions skills/dev-proxy/references/ci-cd-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,8 @@ TOKEN=$(./devproxy/devproxy api token)
curl --noproxy '*' --fail -X POST http://127.0.0.1:8897/proxy -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"recording": true}'
# Run tests
npm test
# Stop recording
# Stop recording; responds after reports are written, e.g.
# {"recording":false,"reports":["/abs/path/ExecutionSummaryPlugin_JsonReporter.json"]}
curl --noproxy '*' --fail -X POST http://127.0.0.1:8897/proxy -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"recording": false}'
# Stop proxy
./devproxy/devproxy stop
Expand All @@ -370,7 +371,7 @@ done
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/proxy` | GET | Get proxy status (`recording`, `configFile`) |
| `/proxy` | POST | Start/stop recording: `{"recording": true/false}` |
| `/proxy` | POST | Start/stop recording: `{"recording": true/false}`. Stopping responds after plugins and reporters finish and returns the absolute paths of the written report files in `reports` |
| `/proxy/stopProxy` | POST | Graceful shutdown |
| `/proxy/jwtToken` | POST | Generate JWT for testing |
| `/proxy/mockRequest` | POST | Trigger mock request (equivalent of pressing `w`) |
Expand Down
Loading