using System.Collections.Specialized; using System.Web; namespace DodoSSH.Client.Auth.Tests; /// /// Stands in for the system browser, and actually fetches the loopback redirect. /// /// /// /// Genuinely drives the client's own over TCP rather than /// injecting a callback result. The listener's request parsing, path filtering and response writing /// are all part of what could break, and none of it is exercised by handing the flow a fabricated /// code. /// /// /// The redirect is fetched on a background task, not awaited inside . The /// client awaits OpenAsync before it begins accepting, so fetching inline would deadlock: the /// browser would be waiting for a response that only arrives once the client starts listening. /// /// internal sealed class FakeBrowser : IBrowserLauncher { private readonly Func>? buildCallback; /// /// Produces the callback query parameters from the authorization request's parameters. Defaults /// to a successful code response echoing the state back. /// internal FakeBrowser( Func>? buildCallback = null) => this.buildCallback = buildCallback; /// The authorization URL the client asked to open. internal Uri? OpenedUrl { get; private set; } /// The authorization request's query parameters. internal NameValueCollection AuthorizeParameters => HttpUtility.ParseQueryString(OpenedUrl?.Query ?? string.Empty); /// The background fetch, so a test can surface its failures. internal Task? CallbackDelivery { get; private set; } /// public Task OpenAsync(Uri url, CancellationToken cancellationToken) { OpenedUrl = url; var parameters = HttpUtility.ParseQueryString(url.Query); var redirectUri = parameters["redirect_uri"] ?? throw new InvalidOperationException("The authorization URL carried no redirect_uri."); var callback = buildCallback is null ? new Dictionary(StringComparer.Ordinal) { ["code"] = "authorization-code", ["state"] = parameters["state"] ?? string.Empty, } : buildCallback(parameters); CallbackDelivery = Task.Run(() => FetchAsync(redirectUri, callback), cancellationToken); return Task.CompletedTask; } private static async Task FetchAsync( string redirectUri, IReadOnlyDictionary callback) { var query = string.Join( '&', callback.Select(p => $"{Uri.EscapeDataString(p.Key)}={Uri.EscapeDataString(p.Value)}")); using var client = new HttpClient(); // A real browser asks for this first. The listener must ignore it rather than treating it as // the callback, so it is part of the flow under test. try { using var favicon = await client.GetAsync(new Uri($"{redirectUri}/../favicon.ico")); } catch (HttpRequestException) { // The listener closes the connection after answering; a transport-level failure here is // not what the test is about. } using var response = await client.GetAsync(new Uri($"{redirectUri}?{query}")); _ = await response.Content.ReadAsStringAsync(); } }