diff --git a/DevProxy.Tests/ApiCommandTests.cs b/DevProxy.Tests/ApiCommandTests.cs new file mode 100644 index 00000000..877366cb --- /dev/null +++ b/DevProxy.Tests/ApiCommandTests.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using DevProxy.Commands; +using Xunit; + +namespace DevProxy.Tests; + +public class ApiCommandTests +{ + [Fact] + public void Endpoints_PostProxy_ShowsRecordingBody() + { + var endpoint = ApiCommand.Endpoints.Single(e => e.Method == "POST" && e.Path == "/proxy"); + + Assert.Contains("body: {\"recording\": true|false}", ApiCommand.FormatDescription(endpoint), StringComparison.Ordinal); + } + + [Fact] + public void Endpoints_RootCertificate_ShowsRequiredFormatQuery() + { + var endpoint = ApiCommand.Endpoints.Single(e => e.Path == "/proxy/rootCertificate"); + + Assert.Contains("query: format=crt", ApiCommand.FormatDescription(endpoint), StringComparison.Ordinal); + } + + [Fact] + public void Endpoints_JwtToken_ShowsBody() + { + var endpoint = ApiCommand.Endpoints.Single(e => e.Path == "/proxy/jwtToken"); + + Assert.Contains("body: {\"name\":", ApiCommand.FormatDescription(endpoint), StringComparison.Ordinal); + } + + [Fact] + public void Endpoints_WithoutInput_ShowDescriptionOnly() + { + var endpoint = ApiCommand.Endpoints.Single(e => e.Path == "/proxy/stopProxy"); + + Assert.Equal("Stop the proxy", ApiCommand.FormatDescription(endpoint)); + } +} diff --git a/DevProxy/Commands/ApiCommand.cs b/DevProxy/Commands/ApiCommand.cs index 78119ddc..43187df1 100644 --- a/DevProxy/Commands/ApiCommand.cs +++ b/DevProxy/Commands/ApiCommand.cs @@ -117,22 +117,38 @@ await Console.Error.WriteLineAsync(pid.HasValue } } + internal static IReadOnlyList Endpoints { get; } = + [ + new() { Method = "GET", Path = "/proxy", Description = "Get proxy status" }, + new() { Method = "POST", Path = "/proxy", Description = "Update proxy status (e.g. start/stop recording)", Body = """{"recording": true|false}""" }, + new() { Method = "POST", Path = "/proxy/mockRequest", Description = "Issue a mock request" }, + new() { Method = "POST", Path = "/proxy/stopProxy", Description = "Stop the proxy" }, + new() { Method = "POST", Path = "/proxy/jwtToken", Description = "Create a JWT token", Body = """{"name": "...", "audiences": ["..."], "issuer": "...", "roles": ["..."], "scopes": ["..."], "claims": {"key": "value"}, "validFor": 60, "signingKey": "..."} (all optional; validFor in minutes)""" }, + new() { Method = "GET", Path = "/proxy/rootCertificate", Description = "Get the root certificate", Query = "format=crt (required)" }, + new() { Method = "GET", Path = "/proxy/logs", Description = "Get proxy logs (for detached mode access)", Query = "lines=50&follow=true&since=5m (all optional)" } + ]; + + internal static string FormatDescription(ApiEndpointInfo endpoint) + { + var description = endpoint.Description; + if (endpoint.Query is not null) + { + description += $" query: {endpoint.Query}"; + } + if (endpoint.Body is not null) + { + description += $" body: {endpoint.Body}"; + } + return description; + } + private void PrintApiInfo(OutputFormat outputFormat) { var apiPort = _proxyConfiguration.ApiPort; var baseUrl = ApiSecurity.GetApiUrl(new UriBuilder(Uri.UriSchemeHttp, _proxyConfiguration.ApiIpAddress, apiPort).Uri.AbsoluteUri); var tokenFilePattern = Path.Combine(StateManager.GetConfigFolder(), "credentials", "api-.token"); - var endpoints = new[] - { - new ApiEndpointInfo { Method = "GET", Path = "/proxy", Description = "Get proxy status" }, - new ApiEndpointInfo { Method = "POST", Path = "/proxy", Description = "Update proxy status (e.g. start/stop recording)" }, - new ApiEndpointInfo { Method = "POST", Path = "/proxy/mockRequest", Description = "Issue a mock request" }, - new ApiEndpointInfo { Method = "POST", Path = "/proxy/stopProxy", Description = "Stop the proxy" }, - new ApiEndpointInfo { Method = "POST", Path = "/proxy/jwtToken", Description = "Create a JWT token" }, - new ApiEndpointInfo { Method = "GET", Path = "/proxy/rootCertificate", Description = "Get the root certificate" }, - new ApiEndpointInfo { Method = "GET", Path = "/proxy/logs", Description = "Get proxy logs (for detached mode access)" } - }; + var endpoints = Endpoints; if (outputFormat == OutputFormat.Json) { @@ -144,7 +160,9 @@ private void PrintApiInfo(OutputFormat outputFormat) { method = e.Method, path = e.Path, - description = e.Description + description = e.Description, + query = e.Query, + body = e.Body }) }, ProxyUtils.JsonSerializerOptions); _logger.LogStructuredOutput(json); @@ -159,7 +177,7 @@ private void PrintApiInfo(OutputFormat outputFormat) _logger.LogInformation("Endpoints:"); foreach (var endpoint in endpoints) { - _logger.LogInformation(" {Method,-6} {Path,-30} {Description}", endpoint.Method, endpoint.Path, endpoint.Description); + _logger.LogInformation(" {Method,-6} {Path,-30} {Description}", endpoint.Method, endpoint.Path, FormatDescription(endpoint)); } } } @@ -170,4 +188,6 @@ sealed class ApiEndpointInfo public string Method { get; set; } = string.Empty; public string Path { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; + public string? Query { get; set; } + public string? Body { get; set; } } \ No newline at end of file