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();
}
}