mirror of
https://github.com/DuendeSoftware/products
synced 2026-05-24 09:28:24 +00:00
Update Console Clients & Aspire version (#2270)
* Updated console client configuration for Windows System Browser console client. Updated Windows System Console so default URL behaviour and callback works. Updated Aspire to v13. * Remove an unused using * Refactored to fix a number of issues and introduce improvements using Spectre Console. * Updated Aspire Test package
This commit is contained in:
parent
ad86e49413
commit
5fa0ca61b2
9 changed files with 572 additions and 127 deletions
|
|
@ -30,9 +30,9 @@ that supports the target frameworks our products target (8, 9, 10) -->
|
|||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="AngleSharp" Version="1.1.2" />
|
||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="9.5.2" />
|
||||
<PackageVersion Include="Aspire.Hosting.Testing" Version="9.5.2" />
|
||||
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="9.5.2" />
|
||||
<PackageVersion Include="Aspire.Hosting.AppHost" Version="13.0.0" />
|
||||
<PackageVersion Include="Aspire.Hosting.Testing" Version="13.0.0" />
|
||||
<PackageVersion Include="Aspire.Hosting.SqlServer" Version="13.0.0" />
|
||||
<PackageVersion Include="BenchmarkDotNet" Version="0.15.0" />
|
||||
<PackageVersion Include="BullsEye" Version="5.0.0" />
|
||||
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
|
||||
|
|
@ -125,6 +125,8 @@ that supports the target frameworks our products target (8, 9, 10) -->
|
|||
<PackageVersion Include="Shouldly" Version="4.2.1" />
|
||||
<PackageVersion Include="SimpleExec" Version="12.0.0" />
|
||||
<PackageVersion Include="SimpleFeedReader" Version="2.0.4" />
|
||||
<PackageVersion Include="Spectre.Console.Cli" Version="0.53.0" />
|
||||
<PackageVersion Include="Spectre.Console.Json" Version="0.53.0" />
|
||||
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="$(IdentityModelVersion)" />
|
||||
<PackageVersion Include="System.Net.Http" Version="4.3.4" />
|
||||
<PackageVersion Include="System.Text.Json" Version="10.0.0" />
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
|
||||
<PackageReference Include="Duende.IdentityModel.OidcClient" />
|
||||
<PackageReference Include="Serilog.AspNetCore" />
|
||||
<PackageReference Include="Spectre.Console.Cli" />
|
||||
<PackageReference Include="Spectre.Console.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace ConsoleResourceIndicators;
|
||||
|
||||
internal enum OutputMode
|
||||
{
|
||||
Verbose,
|
||||
Table
|
||||
}
|
||||
|
|
@ -1,23 +1,60 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Buffers.Text;
|
||||
using System.Text;
|
||||
using Clients;
|
||||
using ConsoleResourceIndicators;
|
||||
using Duende.IdentityModel.Client;
|
||||
using Duende.IdentityModel.OidcClient;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Serilog;
|
||||
using Spectre.Console;
|
||||
|
||||
var builder = Host.CreateApplicationBuilder(args);
|
||||
|
||||
// Add ServiceDefaults from Aspire
|
||||
builder.AddServiceDefaults();
|
||||
|
||||
OidcClient _oidcClient;
|
||||
// Display banner
|
||||
AnsiConsole.Write(new Rule("[bold green]Resource Indicators Demo[/]").Centered());
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
"Resource Indicators Demo".ConsoleBox(ConsoleColor.Green);
|
||||
// Resolve the authority from the configuration
|
||||
var authority = builder.Configuration["is-host"]
|
||||
?? throw new InvalidOperationException("Authority configuration 'is-host' is missing.");
|
||||
|
||||
// Display important setup information
|
||||
var setupPanel = new Panel(
|
||||
new Markup($"[yellow]⚠[/] [bold]Before running tests:[/]\n" +
|
||||
$"[dim]→[/] Ensure your Identity Server is running at: [cyan]{authority}[/]\n" +
|
||||
$"[dim]→[/] Sign in to the Identity Server before starting tests\n" +
|
||||
$"[dim]→[/] This will allow tests to complete quickly and smoothly"))
|
||||
.Border(BoxBorder.Rounded)
|
||||
.BorderColor(Color.Yellow)
|
||||
.Header("[yellow]Setup Checklist[/]");
|
||||
|
||||
AnsiConsole.Write(setupPanel);
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
// Determine output mode based on whether console is interactive
|
||||
OutputMode mode;
|
||||
|
||||
if (Console.IsInputRedirected || Console.IsOutputRedirected || !Environment.UserInteractive)
|
||||
{
|
||||
// Non-interactive environment, use verbose mode by default
|
||||
AnsiConsole.MarkupLine("[dim]Running in non-interactive mode. Using verbose output.[/]");
|
||||
mode = OutputMode.Verbose;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Interactive environment, prompt user for output mode
|
||||
var outputMode = AnsiConsole.Prompt(
|
||||
new SelectionPrompt<string>()
|
||||
.Title("[cyan]Choose output mode:[/]")
|
||||
.AddChoices("Table View (Live Status)", "Verbose Output (Detailed)")
|
||||
.HighlightStyle(new Style(Color.Green)));
|
||||
|
||||
mode = outputMode.StartsWith("Table") ? OutputMode.Table : OutputMode.Verbose;
|
||||
}
|
||||
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
var testRunner = new TestRunner(authority, mode);
|
||||
|
||||
var testsToRun = new List<Test>
|
||||
{
|
||||
|
|
@ -31,138 +68,126 @@ var testsToRun = new List<Test>
|
|||
new() { Id = "8", Enabled = true, Scope = "resource1.scope1 resource2.scope1 resource3.scope1 shared.scope", Resources = ["urn:resource3"] },
|
||||
new() { Id = "9", Enabled = true, Scope = "resource3.scope1 offline_access", Resources = ["urn:resource3"] },
|
||||
new() { Id = "10", Enabled = true, Scope = "resource3.scope1", Resources = ["urn:resource3"] },
|
||||
new() { Id = "11", Enabled = true, Scope = "resource1.scope1 offline_access", Resources = ["urn:resource3"] },
|
||||
new() { Id = "12", Enabled = true, Scope = "shared.scope", Resources = ["urn:invalid"] }
|
||||
new() { Id = "11", Enabled = true, Scope = "resource1.scope1 offline_access", Resources = ["urn:resource3"], AccessTokenExpected = false },
|
||||
new() { Id = "12", Enabled = true, Scope = "shared.scope", Resources = ["urn:invalid"], AccessTokenExpected = false }
|
||||
};
|
||||
|
||||
foreach (var test in testsToRun.Where(t => t.Enabled))
|
||||
await testRunner.RunAllTestsAsync(testsToRun);
|
||||
|
||||
// Show summary
|
||||
AnsiConsole.WriteLine();
|
||||
AnsiConsole.Write(new Rule("[bold green]Test Summary[/]").Centered());
|
||||
|
||||
var summary = new Table()
|
||||
.Border(TableBorder.Rounded)
|
||||
.AddColumn("[bold]Status[/]")
|
||||
.AddColumn("[bold]Count[/]");
|
||||
|
||||
var completed = testsToRun.Count(t => t.Enabled && t.Status == TestStatus.Completed);
|
||||
var failed = testsToRun.Count(t => t.Enabled && t.Status == TestStatus.Failed);
|
||||
var total = testsToRun.Count(t => t.Enabled);
|
||||
|
||||
summary.AddRow("[green]Completed[/]", completed.ToString());
|
||||
summary.AddRow("[red]Failed[/]", failed.ToString());
|
||||
summary.AddRow("[cyan]Total[/]", total.ToString());
|
||||
|
||||
AnsiConsole.Write(summary);
|
||||
|
||||
// Show expected errors section
|
||||
var testsWithExpectedErrors = testsToRun
|
||||
.Where(t => t.Enabled && t.Result?.RefreshResults.Any(r => r.WasExpectedError) == true)
|
||||
.ToList();
|
||||
|
||||
if (testsWithExpectedErrors.Any())
|
||||
{
|
||||
var resources = test.Resources != null ? test.Resources.Aggregate((x, y) => $"{x}, {y}") : "-none-";
|
||||
($"Runing test: ({test.Id}) SCOPES: " + test.Scope + ", RESOURCES: " + resources).ConsoleBox(ConsoleColor.Green);
|
||||
AnsiConsole.WriteLine();
|
||||
AnsiConsole.Write(new Rule("[bold yellow]Expected Errors (By Design)[/]").Centered());
|
||||
AnsiConsole.WriteLine();
|
||||
AnsiConsole.MarkupLine("[dim]The following errors were expected as part of the test validation:[/]");
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
try
|
||||
var expectedErrorsTable = new Table()
|
||||
.Border(TableBorder.Rounded)
|
||||
.AddColumn("[bold]Test ID[/]")
|
||||
.AddColumn("[bold]Resource[/]")
|
||||
.AddColumn("[bold]Error[/]")
|
||||
.AddColumn("[bold]Reason[/]");
|
||||
|
||||
foreach (var test in testsWithExpectedErrors)
|
||||
{
|
||||
await FrontChannel(test.Scope, test.Resources);
|
||||
Thread.Sleep(millisecondsTimeout: 1000);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Exception: {ex.Message}");
|
||||
}
|
||||
}
|
||||
var expectedErrors = test.Result!.RefreshResults.Where(r => r.WasExpectedError).ToList();
|
||||
|
||||
// Exit the application
|
||||
"Exiting application...".ConsoleYellow();
|
||||
Environment.Exit(0);
|
||||
|
||||
async Task FrontChannel(string scope, IEnumerable<string> resource)
|
||||
{
|
||||
// Resolve the authority from the configuration.
|
||||
var authority = builder.Configuration["is-host"];
|
||||
|
||||
resource ??= [];
|
||||
|
||||
// create a redirect URI using an available port on the loopback address.
|
||||
// requires the OP to allow random ports on 127.0.0.1 - otherwise set a static port
|
||||
var browser = new SystemBrowser();
|
||||
var redirectUri = string.Format($"http://127.0.0.1:{browser.Port}");
|
||||
|
||||
var options = new OidcClientOptions
|
||||
{
|
||||
Authority = authority,
|
||||
|
||||
ClientId = "console.resource.indicators",
|
||||
|
||||
RedirectUri = redirectUri,
|
||||
Scope = scope,
|
||||
Resource = [.. resource],
|
||||
FilterClaims = false,
|
||||
LoadProfile = false,
|
||||
Browser = browser,
|
||||
|
||||
Policy =
|
||||
foreach (var error in expectedErrors)
|
||||
{
|
||||
RequireIdentityTokenSignature = false
|
||||
var reason = "Resource not configured for this test";
|
||||
|
||||
expectedErrorsTable.AddRow(
|
||||
test.Id,
|
||||
error.Resource.Replace("urn:", ""),
|
||||
$"[yellow]{error.Error ?? "unknown"}[/]",
|
||||
$"[dim]{reason}[/]"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
var serilog = new LoggerConfiguration()
|
||||
.MinimumLevel.Warning()
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}{Exception}{NewLine}")
|
||||
.CreateLogger();
|
||||
|
||||
options.LoggerFactory.AddSerilog(serilog);
|
||||
|
||||
_oidcClient = new OidcClient(options);
|
||||
var result = await _oidcClient.LoginAsync();
|
||||
|
||||
var parts = result.AccessToken.Split('.');
|
||||
var header = parts[0];
|
||||
var payload = parts[1];
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Standard access token:");
|
||||
Console.WriteLine(Encoding.UTF8.GetString(Base64Url.DecodeFromChars(header)).PrettyPrintJson());
|
||||
Console.WriteLine(Encoding.UTF8.GetString(Base64Url.DecodeFromChars(payload)).PrettyPrintJson());
|
||||
|
||||
if (result.RefreshToken == null)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("No Refresh Token, exiting.");
|
||||
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
await BackChannel(result);
|
||||
AnsiConsole.Write(expectedErrorsTable);
|
||||
}
|
||||
|
||||
async Task BackChannel(LoginResult result)
|
||||
// Show detailed results if available
|
||||
if (mode == OutputMode.Table)
|
||||
{
|
||||
Console.WriteLine("\n\n");
|
||||
Console.WriteLine("Refreshing with resource parameters");
|
||||
var testsWithResults = testsToRun.Where(t => t.Enabled && t.Result != null).ToList();
|
||||
|
||||
var resources = new List<string>() { "urn:resource1", "urn:resource2", "urn:resource3" };
|
||||
|
||||
foreach (var resource in resources)
|
||||
if (testsWithResults.Any())
|
||||
{
|
||||
$"Refreshing for resource: {resource}...".ConsoleGreen();
|
||||
await Refresh(result.RefreshToken, resource);
|
||||
AnsiConsole.WriteLine();
|
||||
AnsiConsole.Write(new Rule("[bold cyan]Detailed Test Results[/]").Centered());
|
||||
|
||||
Thread.Sleep(millisecondsTimeout: 500);
|
||||
}
|
||||
}
|
||||
var detailsTable = new Table()
|
||||
.Border(TableBorder.Rounded)
|
||||
.AddColumn("[bold]Test ID[/]")
|
||||
.AddColumn("[bold]Access Token[/]")
|
||||
.AddColumn("[bold]Refresh Token[/]")
|
||||
.AddColumn("[bold]Refresh Operations[/]");
|
||||
|
||||
async Task Refresh(string refreshToken, string resource)
|
||||
{
|
||||
var result = await _oidcClient.RefreshTokenAsync(refreshToken,
|
||||
new Parameters
|
||||
foreach (var test in testsWithResults)
|
||||
{
|
||||
{ "resource", resource }
|
||||
});
|
||||
var accessToken = test.Result!.AccessTokenReceived
|
||||
? "[green]✓ Received[/]"
|
||||
: test.AccessTokenExpected
|
||||
? "[red]✗ Not Received[/]"
|
||||
: "[green]✓ Not Expected[/]";
|
||||
|
||||
if (result.IsError)
|
||||
{
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(result.Error);
|
||||
return;
|
||||
var refreshToken = test.Result.RefreshTokenReceived
|
||||
? "[green]✓ Received[/]"
|
||||
: test.RefreshTokenExpected
|
||||
? "[red]✗ Not Received[/]"
|
||||
: "[green]✓ Not Expected[/]";
|
||||
|
||||
var refreshOps = test.Result.RefreshResults.Any()
|
||||
? $"{test.Result.RefreshResults.Count(r => r.Success)}/{test.Result.RefreshResults.Count} successful"
|
||||
: "-";
|
||||
|
||||
detailsTable.AddRow(
|
||||
test.Id,
|
||||
accessToken,
|
||||
refreshToken,
|
||||
refreshOps
|
||||
);
|
||||
}
|
||||
|
||||
AnsiConsole.Write(detailsTable);
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("down-scoped access token:");
|
||||
|
||||
var parts = result.AccessToken.Split('.');
|
||||
var header = parts[0];
|
||||
var payload = parts[1];
|
||||
|
||||
Console.WriteLine(Encoding.UTF8.GetString(Base64Url.DecodeFromChars(header)).PrettyPrintJson());
|
||||
Console.WriteLine(Encoding.UTF8.GetString(Base64Url.DecodeFromChars(payload)).PrettyPrintJson());
|
||||
}
|
||||
|
||||
internal class Test
|
||||
// Exit prompt - only in interactive mode
|
||||
if (Environment.UserInteractive && !Console.IsInputRedirected)
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
public string Scope { get; set; }
|
||||
public IEnumerable<string> Resources { get; set; } = null;
|
||||
AnsiConsole.WriteLine();
|
||||
AnsiConsole.Markup("[dim]Press Enter to exit...[/]");
|
||||
Console.ReadLine();
|
||||
}
|
||||
else
|
||||
{
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
namespace ConsoleResourceIndicators;
|
||||
|
||||
internal enum TestStatus
|
||||
{
|
||||
Pending,
|
||||
Running,
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
|
||||
internal class TestResult
|
||||
{
|
||||
public bool AccessTokenReceived { get; set; }
|
||||
public bool RefreshTokenReceived { get; set; }
|
||||
public List<RefreshResult> RefreshResults { get; set; } = [];
|
||||
}
|
||||
|
||||
internal class RefreshResult
|
||||
{
|
||||
public string Resource { get; set; } = string.Empty;
|
||||
public bool Success { get; set; }
|
||||
public string Error { get; set; }
|
||||
public bool WasExpectedError { get; set; }
|
||||
}
|
||||
|
||||
internal class Test
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public bool Enabled { get; set; }
|
||||
public bool AccessTokenExpected { get; set; } = true;
|
||||
public bool RefreshTokenExpected => Scope.Contains("offline_access") && AccessTokenExpected;
|
||||
public string Scope { get; set; } = string.Empty;
|
||||
public IEnumerable<string> Resources { get; set; } = [];
|
||||
public TestStatus Status { get; set; } = TestStatus.Pending;
|
||||
public string ErrorMessage { get; set; }
|
||||
public DateTime? StartTime { get; set; }
|
||||
public DateTime? EndTime { get; set; }
|
||||
public TestResult Result { get; set; }
|
||||
}
|
||||
|
|
@ -0,0 +1,360 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Buffers.Text;
|
||||
using System.Text;
|
||||
using Duende.IdentityModel.Client;
|
||||
using Duende.IdentityModel.OidcClient;
|
||||
using Spectre.Console;
|
||||
using Spectre.Console.Json;
|
||||
|
||||
namespace ConsoleResourceIndicators;
|
||||
|
||||
internal class TestRunner(string authority, OutputMode outputMode)
|
||||
{
|
||||
private readonly string _authority = authority;
|
||||
private readonly OutputMode _outputMode = outputMode;
|
||||
private OidcClient _oidcClient;
|
||||
|
||||
private static readonly string[] RefreshResources = ["urn:resource1", "urn:resource2", "urn:resource3"];
|
||||
private const int DelayBetweenTestsMs = 1000;
|
||||
private const int DelayBetweenRefreshMs = 500;
|
||||
|
||||
public async Task RunAllTestsAsync(List<Test> tests)
|
||||
{
|
||||
if (_outputMode == OutputMode.Table)
|
||||
{
|
||||
await RunTestsWithTableAsync(tests);
|
||||
}
|
||||
else
|
||||
{
|
||||
await RunTestsVerboseAsync(tests);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunTestsVerboseAsync(List<Test> tests)
|
||||
{
|
||||
foreach (var test in tests.Where(t => t.Enabled))
|
||||
{
|
||||
await RunTestVerboseAsync(test);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunTestsWithTableAsync(List<Test> tests)
|
||||
{
|
||||
var enabledTests = tests.Where(t => t.Enabled).ToList();
|
||||
|
||||
await AnsiConsole.Live(CreateTestTable(enabledTests))
|
||||
.StartAsync(async ctx =>
|
||||
{
|
||||
foreach (var test in enabledTests)
|
||||
{
|
||||
test.Status = TestStatus.Running;
|
||||
test.StartTime = DateTime.Now;
|
||||
ctx.UpdateTarget(CreateTestTable(enabledTests));
|
||||
|
||||
try
|
||||
{
|
||||
await ExecuteTestAsync(test, verbose: false);
|
||||
test.Status = TestStatus.Completed;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
test.Status = TestStatus.Failed;
|
||||
test.ErrorMessage = ex.Message;
|
||||
}
|
||||
|
||||
test.EndTime = DateTime.Now;
|
||||
ctx.UpdateTarget(CreateTestTable(enabledTests));
|
||||
await Task.Delay(DelayBetweenTestsMs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static Table CreateTestTable(List<Test> tests)
|
||||
{
|
||||
var table = new Table()
|
||||
.Border(TableBorder.Rounded)
|
||||
.AddColumn("[bold]ID[/]")
|
||||
.AddColumn("[bold]Status[/]")
|
||||
.AddColumn("[bold]Scopes[/]")
|
||||
.AddColumn("[bold]Resources[/]")
|
||||
.AddColumn("[bold]Duration[/]");
|
||||
|
||||
foreach (var test in tests)
|
||||
{
|
||||
var status = test.Status switch
|
||||
{
|
||||
TestStatus.Pending => "[grey]Pending[/]",
|
||||
TestStatus.Running => "[yellow]Running...[/]",
|
||||
TestStatus.Completed => "[green]✓ Completed[/]",
|
||||
TestStatus.Failed => $"[red]✗ Failed[/]",
|
||||
_ => "[grey]Unknown[/]"
|
||||
};
|
||||
|
||||
var resourcesList = test.Resources?.Any() == true
|
||||
? string.Join(", ", test.Resources.Select(r => r.Replace("urn:", "")))
|
||||
: "-";
|
||||
|
||||
var duration = test.StartTime.HasValue && test.EndTime.HasValue
|
||||
? $"{(test.EndTime.Value - test.StartTime.Value).TotalSeconds:F1}s"
|
||||
: test.StartTime.HasValue
|
||||
? "..."
|
||||
: "-";
|
||||
|
||||
// Truncate scopes for table display
|
||||
var scopeDisplay = test.Scope.Length > 40
|
||||
? string.Concat(test.Scope.AsSpan(0, 37), "...")
|
||||
: test.Scope;
|
||||
|
||||
table.AddRow(
|
||||
test.Id,
|
||||
status,
|
||||
scopeDisplay,
|
||||
resourcesList,
|
||||
duration
|
||||
);
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
private async Task RunTestVerboseAsync(Test test)
|
||||
{
|
||||
var resourcesList = test.Resources?.Any() == true
|
||||
? string.Join(", ", test.Resources)
|
||||
: "-none-";
|
||||
|
||||
// Escape the text to prevent Spectre.Console from interpreting it as markup
|
||||
var scopeText = test.Scope.EscapeMarkup();
|
||||
var resourcesText = resourcesList.EscapeMarkup();
|
||||
|
||||
var panel = new Panel(
|
||||
new Markup($"[bold]Test {test.Id}[/]\n" +
|
||||
$"[dim]Scopes:[/] {scopeText}\n" +
|
||||
$"[dim]Resources:[/] {resourcesText}"))
|
||||
.Border(BoxBorder.Rounded)
|
||||
.BorderColor(Color.Blue)
|
||||
.Header("[blue]Running Test[/]");
|
||||
|
||||
AnsiConsole.Write(panel);
|
||||
|
||||
try
|
||||
{
|
||||
test.Status = TestStatus.Running;
|
||||
test.StartTime = DateTime.Now;
|
||||
await ExecuteTestAsync(test, verbose: true);
|
||||
test.Status = TestStatus.Completed;
|
||||
test.EndTime = DateTime.Now;
|
||||
AnsiConsole.MarkupLine("[green]✓ Test completed successfully[/]\n");
|
||||
await Task.Delay(DelayBetweenTestsMs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
test.Status = TestStatus.Failed;
|
||||
test.ErrorMessage = ex.Message;
|
||||
test.EndTime = DateTime.Now;
|
||||
AnsiConsole.MarkupLine($"[red]✗ Test failed: {Markup.Escape(ex.Message)}[/]\n");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ExecuteTestAsync(Test test, bool verbose)
|
||||
{
|
||||
test.Result = new TestResult();
|
||||
|
||||
var browser = new SystemBrowser();
|
||||
var redirectUri = $"http://127.0.0.1:{browser.Port}";
|
||||
|
||||
var options = new OidcClientOptions
|
||||
{
|
||||
Authority = _authority,
|
||||
ClientId = "console.resource.indicators",
|
||||
RedirectUri = redirectUri,
|
||||
Scope = test.Scope,
|
||||
Resource = test.Resources?.ToList() ?? [],
|
||||
FilterClaims = false,
|
||||
LoadProfile = false,
|
||||
Browser = browser,
|
||||
Policy =
|
||||
{
|
||||
RequireIdentityTokenSignature = false
|
||||
}
|
||||
};
|
||||
|
||||
_oidcClient = new OidcClient(options);
|
||||
var result = await _oidcClient.LoginAsync();
|
||||
|
||||
test.Result.AccessTokenReceived = result.AccessToken != null;
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
HandleAccessTokenVerbose(result.AccessToken, test.AccessTokenExpected);
|
||||
|
||||
if (test.AccessTokenExpected && test.RefreshTokenExpected)
|
||||
{
|
||||
test.Result.RefreshTokenReceived = result.RefreshToken != null;
|
||||
await HandleRefreshTokenVerbose(result, test.Resources, test.Result);
|
||||
}
|
||||
else if (!test.RefreshTokenExpected)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[green]✓ Refresh Token was not expected and not received[/]");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// In table mode, just validate and collect results without output
|
||||
if (test.AccessTokenExpected && result.AccessToken == null)
|
||||
{
|
||||
throw new Exception("Access token expected but not received");
|
||||
}
|
||||
|
||||
if (test.AccessTokenExpected && test.RefreshTokenExpected)
|
||||
{
|
||||
if (result.RefreshToken == null)
|
||||
{
|
||||
throw new Exception("Refresh token expected but not received");
|
||||
}
|
||||
test.Result.RefreshTokenReceived = true;
|
||||
await HandleRefreshTokenSilent(result, test.Resources, test.Result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleAccessTokenVerbose(string accessToken, bool expected)
|
||||
{
|
||||
if (expected)
|
||||
{
|
||||
if (accessToken is null)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]✗ An Access Token was expected but not received[/]");
|
||||
return;
|
||||
}
|
||||
|
||||
AnsiConsole.MarkupLine("[green]✓ Access Token received[/]");
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
PrintJwtToken(accessToken, "Standard Access Token");
|
||||
}
|
||||
else
|
||||
{
|
||||
AnsiConsole.MarkupLine("[green]✓ Access Token was not expected and not received[/]");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleRefreshTokenVerbose(LoginResult result, IEnumerable<string> testResources, TestResult testResult)
|
||||
{
|
||||
if (result.RefreshToken is null)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]✗ A Refresh Token was expected but not received[/]");
|
||||
return;
|
||||
}
|
||||
|
||||
AnsiConsole.WriteLine();
|
||||
AnsiConsole.WriteLine();
|
||||
|
||||
AnsiConsole.Write(new Rule("[yellow]Refreshing with Resource Parameters[/]").LeftJustified());
|
||||
|
||||
var resourcesSet = testResources?.ToHashSet() ?? [];
|
||||
|
||||
foreach (var resource in RefreshResources)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[cyan]→ Refreshing for resource: {resource}[/]");
|
||||
await RefreshTokenAsync(result.RefreshToken, resource, resourcesSet.Contains(resource), verbose: true, testResult);
|
||||
await Task.Delay(DelayBetweenRefreshMs);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleRefreshTokenSilent(LoginResult result, IEnumerable<string> testResources, TestResult testResult)
|
||||
{
|
||||
var resourcesSet = testResources?.ToHashSet() ?? [];
|
||||
|
||||
foreach (var resource in RefreshResources)
|
||||
{
|
||||
await RefreshTokenAsync(result.RefreshToken!, resource, resourcesSet.Contains(resource), verbose: false, testResult);
|
||||
await Task.Delay(DelayBetweenRefreshMs);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshTokenAsync(string refreshToken, string resource, bool resourceIsConfigured, bool verbose, TestResult testResult)
|
||||
{
|
||||
if (_oidcClient == null)
|
||||
{
|
||||
throw new InvalidOperationException("OIDC client not initialized");
|
||||
}
|
||||
|
||||
var result = await _oidcClient.RefreshTokenAsync(refreshToken,
|
||||
new Parameters
|
||||
{
|
||||
{ "resource", resource }
|
||||
});
|
||||
|
||||
var refreshResult = new RefreshResult
|
||||
{
|
||||
Resource = resource,
|
||||
Success = !result.IsError,
|
||||
Error = result.Error,
|
||||
WasExpectedError = !resourceIsConfigured && result.IsError
|
||||
};
|
||||
|
||||
testResult.RefreshResults.Add(refreshResult);
|
||||
|
||||
if (result.IsError)
|
||||
{
|
||||
if (resourceIsConfigured)
|
||||
{
|
||||
var message = $"An error was not expected but was received: {result.Error}";
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.MarkupLine($"[red]✗ {Markup.Escape(message)}[/]");
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception(message);
|
||||
}
|
||||
}
|
||||
else if (verbose)
|
||||
{
|
||||
// Expected error - show in verbose mode
|
||||
AnsiConsole.MarkupLine($"[green]✓ Expected error received: [/][yellow]{Markup.Escape(result.Error ?? "unknown")}[/]");
|
||||
}
|
||||
// In non-verbose mode, we don't show expected errors here - they'll be in the summary
|
||||
return;
|
||||
}
|
||||
|
||||
if (verbose)
|
||||
{
|
||||
AnsiConsole.WriteLine();
|
||||
PrintJwtToken(result.AccessToken!, "Down-scoped access token");
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrintJwtToken(string token, string blockHeader = "JWT Token")
|
||||
{
|
||||
var parts = token.Split('.');
|
||||
if (parts.Length < 2)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[red]Invalid JWT token format[/]");
|
||||
return;
|
||||
}
|
||||
|
||||
var header = parts[0];
|
||||
var payload = parts[1];
|
||||
|
||||
var headerJson = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(header));
|
||||
var payloadJson = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(payload));
|
||||
|
||||
// Use Spectre.Console's built-in JSON rendering with proper namespace
|
||||
AnsiConsole.Write(
|
||||
new Panel(
|
||||
new Rows(
|
||||
new Markup("[bold yellow]Header:[/]"),
|
||||
new JsonText(headerJson),
|
||||
new Text(""),
|
||||
new Markup("[bold yellow]Payload:[/]"),
|
||||
new JsonText(payloadJson)
|
||||
))
|
||||
.Border(BoxBorder.Rounded)
|
||||
.BorderColor(Color.Grey)
|
||||
.Header($"[dim]{blockHeader}[/]"));
|
||||
}
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ internal class Program
|
|||
{
|
||||
Authority = Constants.Authority,
|
||||
ClientId = "winconsole",
|
||||
Scope = "openid profile scope1",
|
||||
Scope = "openid profile resource1.scope1",
|
||||
RedirectUri = redirectUri,
|
||||
};
|
||||
|
||||
|
|
@ -94,7 +94,12 @@ internal class Program
|
|||
var callbackManager = new CallbackManager(state.State);
|
||||
|
||||
// open system browser to start authentication
|
||||
Process.Start(state.StartUrl);
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = state.StartUrl,
|
||||
UseShellExecute = true
|
||||
};
|
||||
Process.Start(psi);
|
||||
|
||||
Console.WriteLine("Running callback manager");
|
||||
var response = await callbackManager.RunServer();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright (c) Duende Software. All rights reserved.
|
||||
// See LICENSE in the project root for license information.
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Win32;
|
||||
|
||||
|
|
@ -36,7 +35,7 @@ internal class RegistryConfig
|
|||
|
||||
private const string CommandKeyValueName = "";
|
||||
private const string CommandKeyValueFormat = "\"{0}\" \"%1\"";
|
||||
private static string CommandKeyValueValue => string.Format(CommandKeyValueFormat, Assembly.GetExecutingAssembly().Location);
|
||||
private static string CommandKeyValueValue => string.Format(CommandKeyValueFormat, Environment.ProcessPath);
|
||||
|
||||
private const string UrlProtocolValueName = "URL Protocol";
|
||||
private const string UrlProtocolValueValue = "";
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ public static class ClientsConsole
|
|||
RedirectUris = { "sample-windows-client://callback" },
|
||||
RequireConsent = false,
|
||||
AllowOfflineAccess = true,
|
||||
AllowedIdentityTokenSigningAlgorithms = { "ES256" },
|
||||
AllowedIdentityTokenSigningAlgorithms = { "RS256" },
|
||||
AllowedScopes =
|
||||
{
|
||||
IdentityServerConstants.StandardScopes.OpenId,
|
||||
|
|
|
|||
Loading…
Reference in a new issue