yavsc/src/PostIt.Tests/FakeAuthorizingBrowser.cs

97 lines
3.7 KiB
C#
Raw Normal View History

using System;
using System.Net.Http;
using System.Threading.Tasks;
using IdentityModel.OidcClient.Browser;
namespace PostIt.Tests;
/// <summary>
/// A minimal <see cref="IBrowser"/> for tests. Captures the authorize
/// URL emitted by OidcClient, extracts its <c>state</c>, and returns a
/// BrowserResult that mimics the OIDC redirect-with-code callback.
///
2026-07-05 23:56:10 +01:00
/// The paired <see cref="OIDCStubAuthority"/>'s token endpoint accepts
/// any authorization code, so we don't need to mint a real one here.
/// </summary>
public sealed class FakeAuthorizingBrowser
{
postit: replace loopback HTTP listener with custom URI scheme OAuth2 redirect handling for the desktop PostIt app now follows RFC 8252 §7.1: the redirect URI is a custom scheme (postit://callback) that the OS routes back to PostIt instead of a 127.0.0.1 HTTP listener. The browser hits the scheme, the OS launches a fresh PostIt process, that process hands the URL to the running instance over a named pipe, then exits. Architecture: - SingleInstance: cross-platform named-pipe helper. TryHandOffAsync is what the 2nd instance calls to forward its command-line URL; StartServerAsync runs on the 1st instance and pumps URLs into a callback (the running CustomSchemeBrowser). - CustomSchemeBrowser: IBrowser that opens the system browser on the authorize URL and blocks until the named pipe yields the callback URL. No HTTP listener, no port to bind or release, no HttpListener lifecycle to babysit. - Platform.DefaultRedirectUri is now postit://callback. The CustomScheme property exposes the prefix for the redirect validator. - App.OnFrameworkInitializationCompleted detects a 2nd-instance launch by scanning command-line args for the scheme prefix, hands the URL off, and exits before opening a window. The first instance starts normally and only the browser is replaced. What still has to happen on the user's machine: - Registering the postit:// scheme with the OS (a one-time setup step: .desktop file on Linux, registry key on Windows, Info.plist / LSSetDefaultHandlerForURLScheme on macOS). The code already validates EndUrl starts with the configured scheme, so a missing registration surfaces as a clear error from CustomSchemeBrowser rather than a silent hang. Removed: - LoopbackBrowser and LoopbackBrowserTests — the listener, the timeout, the double Stop/Close dance. The whole class of 'port already bound' / 'next launch fails' issues goes away. - The /tests/LoopbackBrowserTests.cs regression coverage is obsolete: there is no listener to release anymore. The single- instance hand-off is covered by the existing tests on the OidcClient flow path. Tests: PostIt.Tests 17/17 pass, Yavsc.Org.Tests 13/14 (the one remaining failure is SendEMailSynchrone, pre-existing and unrelated to this change).
2026-06-22 00:53:01 +01:00
private readonly string _redirectUri;
private readonly HttpClient _http = new();
postit: replace loopback HTTP listener with custom URI scheme OAuth2 redirect handling for the desktop PostIt app now follows RFC 8252 §7.1: the redirect URI is a custom scheme (postit://callback) that the OS routes back to PostIt instead of a 127.0.0.1 HTTP listener. The browser hits the scheme, the OS launches a fresh PostIt process, that process hands the URL to the running instance over a named pipe, then exits. Architecture: - SingleInstance: cross-platform named-pipe helper. TryHandOffAsync is what the 2nd instance calls to forward its command-line URL; StartServerAsync runs on the 1st instance and pumps URLs into a callback (the running CustomSchemeBrowser). - CustomSchemeBrowser: IBrowser that opens the system browser on the authorize URL and blocks until the named pipe yields the callback URL. No HTTP listener, no port to bind or release, no HttpListener lifecycle to babysit. - Platform.DefaultRedirectUri is now postit://callback. The CustomScheme property exposes the prefix for the redirect validator. - App.OnFrameworkInitializationCompleted detects a 2nd-instance launch by scanning command-line args for the scheme prefix, hands the URL off, and exits before opening a window. The first instance starts normally and only the browser is replaced. What still has to happen on the user's machine: - Registering the postit:// scheme with the OS (a one-time setup step: .desktop file on Linux, registry key on Windows, Info.plist / LSSetDefaultHandlerForURLScheme on macOS). The code already validates EndUrl starts with the configured scheme, so a missing registration surfaces as a clear error from CustomSchemeBrowser rather than a silent hang. Removed: - LoopbackBrowser and LoopbackBrowserTests — the listener, the timeout, the double Stop/Close dance. The whole class of 'port already bound' / 'next launch fails' issues goes away. - The /tests/LoopbackBrowserTests.cs regression coverage is obsolete: there is no listener to release anymore. The single- instance hand-off is covered by the existing tests on the OidcClient flow path. Tests: PostIt.Tests 17/17 pass, Yavsc.Org.Tests 13/14 (the one remaining failure is SendEMailSynchrone, pre-existing and unrelated to this change).
2026-06-22 00:53:01 +01:00
public FakeAuthorizingBrowser(string redirectUri)
{
postit: replace loopback HTTP listener with custom URI scheme OAuth2 redirect handling for the desktop PostIt app now follows RFC 8252 §7.1: the redirect URI is a custom scheme (postit://callback) that the OS routes back to PostIt instead of a 127.0.0.1 HTTP listener. The browser hits the scheme, the OS launches a fresh PostIt process, that process hands the URL to the running instance over a named pipe, then exits. Architecture: - SingleInstance: cross-platform named-pipe helper. TryHandOffAsync is what the 2nd instance calls to forward its command-line URL; StartServerAsync runs on the 1st instance and pumps URLs into a callback (the running CustomSchemeBrowser). - CustomSchemeBrowser: IBrowser that opens the system browser on the authorize URL and blocks until the named pipe yields the callback URL. No HTTP listener, no port to bind or release, no HttpListener lifecycle to babysit. - Platform.DefaultRedirectUri is now postit://callback. The CustomScheme property exposes the prefix for the redirect validator. - App.OnFrameworkInitializationCompleted detects a 2nd-instance launch by scanning command-line args for the scheme prefix, hands the URL off, and exits before opening a window. The first instance starts normally and only the browser is replaced. What still has to happen on the user's machine: - Registering the postit:// scheme with the OS (a one-time setup step: .desktop file on Linux, registry key on Windows, Info.plist / LSSetDefaultHandlerForURLScheme on macOS). The code already validates EndUrl starts with the configured scheme, so a missing registration surfaces as a clear error from CustomSchemeBrowser rather than a silent hang. Removed: - LoopbackBrowser and LoopbackBrowserTests — the listener, the timeout, the double Stop/Close dance. The whole class of 'port already bound' / 'next launch fails' issues goes away. - The /tests/LoopbackBrowserTests.cs regression coverage is obsolete: there is no listener to release anymore. The single- instance hand-off is covered by the existing tests on the OidcClient flow path. Tests: PostIt.Tests 17/17 pass, Yavsc.Org.Tests 13/14 (the one remaining failure is SendEMailSynchrone, pre-existing and unrelated to this change).
2026-06-22 00:53:01 +01:00
_redirectUri = redirectUri;
}
postit: replace loopback HTTP listener with custom URI scheme OAuth2 redirect handling for the desktop PostIt app now follows RFC 8252 §7.1: the redirect URI is a custom scheme (postit://callback) that the OS routes back to PostIt instead of a 127.0.0.1 HTTP listener. The browser hits the scheme, the OS launches a fresh PostIt process, that process hands the URL to the running instance over a named pipe, then exits. Architecture: - SingleInstance: cross-platform named-pipe helper. TryHandOffAsync is what the 2nd instance calls to forward its command-line URL; StartServerAsync runs on the 1st instance and pumps URLs into a callback (the running CustomSchemeBrowser). - CustomSchemeBrowser: IBrowser that opens the system browser on the authorize URL and blocks until the named pipe yields the callback URL. No HTTP listener, no port to bind or release, no HttpListener lifecycle to babysit. - Platform.DefaultRedirectUri is now postit://callback. The CustomScheme property exposes the prefix for the redirect validator. - App.OnFrameworkInitializationCompleted detects a 2nd-instance launch by scanning command-line args for the scheme prefix, hands the URL off, and exits before opening a window. The first instance starts normally and only the browser is replaced. What still has to happen on the user's machine: - Registering the postit:// scheme with the OS (a one-time setup step: .desktop file on Linux, registry key on Windows, Info.plist / LSSetDefaultHandlerForURLScheme on macOS). The code already validates EndUrl starts with the configured scheme, so a missing registration surfaces as a clear error from CustomSchemeBrowser rather than a silent hang. Removed: - LoopbackBrowser and LoopbackBrowserTests — the listener, the timeout, the double Stop/Close dance. The whole class of 'port already bound' / 'next launch fails' issues goes away. - The /tests/LoopbackBrowserTests.cs regression coverage is obsolete: there is no listener to release anymore. The single- instance hand-off is covered by the existing tests on the OidcClient flow path. Tests: PostIt.Tests 17/17 pass, Yavsc.Org.Tests 13/14 (the one remaining failure is SendEMailSynchrone, pre-existing and unrelated to this change).
2026-06-22 00:53:01 +01:00
public IdentityModel.OidcClient.Browser.IBrowser CreateBrowser() => new Impl(_redirectUri, _http);
private sealed class Impl : IdentityModel.OidcClient.Browser.IBrowser
{
postit: replace loopback HTTP listener with custom URI scheme OAuth2 redirect handling for the desktop PostIt app now follows RFC 8252 §7.1: the redirect URI is a custom scheme (postit://callback) that the OS routes back to PostIt instead of a 127.0.0.1 HTTP listener. The browser hits the scheme, the OS launches a fresh PostIt process, that process hands the URL to the running instance over a named pipe, then exits. Architecture: - SingleInstance: cross-platform named-pipe helper. TryHandOffAsync is what the 2nd instance calls to forward its command-line URL; StartServerAsync runs on the 1st instance and pumps URLs into a callback (the running CustomSchemeBrowser). - CustomSchemeBrowser: IBrowser that opens the system browser on the authorize URL and blocks until the named pipe yields the callback URL. No HTTP listener, no port to bind or release, no HttpListener lifecycle to babysit. - Platform.DefaultRedirectUri is now postit://callback. The CustomScheme property exposes the prefix for the redirect validator. - App.OnFrameworkInitializationCompleted detects a 2nd-instance launch by scanning command-line args for the scheme prefix, hands the URL off, and exits before opening a window. The first instance starts normally and only the browser is replaced. What still has to happen on the user's machine: - Registering the postit:// scheme with the OS (a one-time setup step: .desktop file on Linux, registry key on Windows, Info.plist / LSSetDefaultHandlerForURLScheme on macOS). The code already validates EndUrl starts with the configured scheme, so a missing registration surfaces as a clear error from CustomSchemeBrowser rather than a silent hang. Removed: - LoopbackBrowser and LoopbackBrowserTests — the listener, the timeout, the double Stop/Close dance. The whole class of 'port already bound' / 'next launch fails' issues goes away. - The /tests/LoopbackBrowserTests.cs regression coverage is obsolete: there is no listener to release anymore. The single- instance hand-off is covered by the existing tests on the OidcClient flow path. Tests: PostIt.Tests 17/17 pass, Yavsc.Org.Tests 13/14 (the one remaining failure is SendEMailSynchrone, pre-existing and unrelated to this change).
2026-06-22 00:53:01 +01:00
private readonly string _redirectUri;
private readonly HttpClient _http;
postit: replace loopback HTTP listener with custom URI scheme OAuth2 redirect handling for the desktop PostIt app now follows RFC 8252 §7.1: the redirect URI is a custom scheme (postit://callback) that the OS routes back to PostIt instead of a 127.0.0.1 HTTP listener. The browser hits the scheme, the OS launches a fresh PostIt process, that process hands the URL to the running instance over a named pipe, then exits. Architecture: - SingleInstance: cross-platform named-pipe helper. TryHandOffAsync is what the 2nd instance calls to forward its command-line URL; StartServerAsync runs on the 1st instance and pumps URLs into a callback (the running CustomSchemeBrowser). - CustomSchemeBrowser: IBrowser that opens the system browser on the authorize URL and blocks until the named pipe yields the callback URL. No HTTP listener, no port to bind or release, no HttpListener lifecycle to babysit. - Platform.DefaultRedirectUri is now postit://callback. The CustomScheme property exposes the prefix for the redirect validator. - App.OnFrameworkInitializationCompleted detects a 2nd-instance launch by scanning command-line args for the scheme prefix, hands the URL off, and exits before opening a window. The first instance starts normally and only the browser is replaced. What still has to happen on the user's machine: - Registering the postit:// scheme with the OS (a one-time setup step: .desktop file on Linux, registry key on Windows, Info.plist / LSSetDefaultHandlerForURLScheme on macOS). The code already validates EndUrl starts with the configured scheme, so a missing registration surfaces as a clear error from CustomSchemeBrowser rather than a silent hang. Removed: - LoopbackBrowser and LoopbackBrowserTests — the listener, the timeout, the double Stop/Close dance. The whole class of 'port already bound' / 'next launch fails' issues goes away. - The /tests/LoopbackBrowserTests.cs regression coverage is obsolete: there is no listener to release anymore. The single- instance hand-off is covered by the existing tests on the OidcClient flow path. Tests: PostIt.Tests 17/17 pass, Yavsc.Org.Tests 13/14 (the one remaining failure is SendEMailSynchrone, pre-existing and unrelated to this change).
2026-06-22 00:53:01 +01:00
public Impl(string redirectUri, HttpClient http)
{
postit: replace loopback HTTP listener with custom URI scheme OAuth2 redirect handling for the desktop PostIt app now follows RFC 8252 §7.1: the redirect URI is a custom scheme (postit://callback) that the OS routes back to PostIt instead of a 127.0.0.1 HTTP listener. The browser hits the scheme, the OS launches a fresh PostIt process, that process hands the URL to the running instance over a named pipe, then exits. Architecture: - SingleInstance: cross-platform named-pipe helper. TryHandOffAsync is what the 2nd instance calls to forward its command-line URL; StartServerAsync runs on the 1st instance and pumps URLs into a callback (the running CustomSchemeBrowser). - CustomSchemeBrowser: IBrowser that opens the system browser on the authorize URL and blocks until the named pipe yields the callback URL. No HTTP listener, no port to bind or release, no HttpListener lifecycle to babysit. - Platform.DefaultRedirectUri is now postit://callback. The CustomScheme property exposes the prefix for the redirect validator. - App.OnFrameworkInitializationCompleted detects a 2nd-instance launch by scanning command-line args for the scheme prefix, hands the URL off, and exits before opening a window. The first instance starts normally and only the browser is replaced. What still has to happen on the user's machine: - Registering the postit:// scheme with the OS (a one-time setup step: .desktop file on Linux, registry key on Windows, Info.plist / LSSetDefaultHandlerForURLScheme on macOS). The code already validates EndUrl starts with the configured scheme, so a missing registration surfaces as a clear error from CustomSchemeBrowser rather than a silent hang. Removed: - LoopbackBrowser and LoopbackBrowserTests — the listener, the timeout, the double Stop/Close dance. The whole class of 'port already bound' / 'next launch fails' issues goes away. - The /tests/LoopbackBrowserTests.cs regression coverage is obsolete: there is no listener to release anymore. The single- instance hand-off is covered by the existing tests on the OidcClient flow path. Tests: PostIt.Tests 17/17 pass, Yavsc.Org.Tests 13/14 (the one remaining failure is SendEMailSynchrone, pre-existing and unrelated to this change).
2026-06-22 00:53:01 +01:00
_redirectUri = redirectUri;
_http = http;
}
public async Task<BrowserResult> InvokeAsync(BrowserOptions options, System.Threading.CancellationToken cancellationToken = default)
{
// Touch the authorize URL so any 4xx/5xx surfaces; we don't
// actually need its response body because we synthesize the
// redirect below from the original URL's query string.
var startUri = new Uri(options.StartUrl);
try
{
using var resp = await _http.GetAsync(startUri, cancellationToken);
// Ignore the status: the stub has no real /connect/authorize.
}
catch
{
// Network errors are expected against the stub; continue.
}
// Pull `state` from the authorize URL so the OidcClient can
// verify it against its own nonces.
var state = ParseQuery(startUri.Query).GetValueOrDefault("state");
if (string.IsNullOrEmpty(state))
{
return new BrowserResult
{
ResultType = BrowserResultType.UserCancel,
ErrorDescription = "no state in authorize URL"
};
}
postit: replace loopback HTTP listener with custom URI scheme OAuth2 redirect handling for the desktop PostIt app now follows RFC 8252 §7.1: the redirect URI is a custom scheme (postit://callback) that the OS routes back to PostIt instead of a 127.0.0.1 HTTP listener. The browser hits the scheme, the OS launches a fresh PostIt process, that process hands the URL to the running instance over a named pipe, then exits. Architecture: - SingleInstance: cross-platform named-pipe helper. TryHandOffAsync is what the 2nd instance calls to forward its command-line URL; StartServerAsync runs on the 1st instance and pumps URLs into a callback (the running CustomSchemeBrowser). - CustomSchemeBrowser: IBrowser that opens the system browser on the authorize URL and blocks until the named pipe yields the callback URL. No HTTP listener, no port to bind or release, no HttpListener lifecycle to babysit. - Platform.DefaultRedirectUri is now postit://callback. The CustomScheme property exposes the prefix for the redirect validator. - App.OnFrameworkInitializationCompleted detects a 2nd-instance launch by scanning command-line args for the scheme prefix, hands the URL off, and exits before opening a window. The first instance starts normally and only the browser is replaced. What still has to happen on the user's machine: - Registering the postit:// scheme with the OS (a one-time setup step: .desktop file on Linux, registry key on Windows, Info.plist / LSSetDefaultHandlerForURLScheme on macOS). The code already validates EndUrl starts with the configured scheme, so a missing registration surfaces as a clear error from CustomSchemeBrowser rather than a silent hang. Removed: - LoopbackBrowser and LoopbackBrowserTests — the listener, the timeout, the double Stop/Close dance. The whole class of 'port already bound' / 'next launch fails' issues goes away. - The /tests/LoopbackBrowserTests.cs regression coverage is obsolete: there is no listener to release anymore. The single- instance hand-off is covered by the existing tests on the OidcClient flow path. Tests: PostIt.Tests 17/17 pass, Yavsc.Org.Tests 13/14 (the one remaining failure is SendEMailSynchrone, pre-existing and unrelated to this change).
2026-06-22 00:53:01 +01:00
// Synthesize the redirect that the OIDC server would have
// sent back. The scheme and path match whatever the test
// configured (loopback for the historical test harness,
// postit://callback for the custom-scheme path).
var baseUri = _redirectUri;
if (!baseUri.EndsWith("/")) baseUri += "/";
var redirectUri =
postit: replace loopback HTTP listener with custom URI scheme OAuth2 redirect handling for the desktop PostIt app now follows RFC 8252 §7.1: the redirect URI is a custom scheme (postit://callback) that the OS routes back to PostIt instead of a 127.0.0.1 HTTP listener. The browser hits the scheme, the OS launches a fresh PostIt process, that process hands the URL to the running instance over a named pipe, then exits. Architecture: - SingleInstance: cross-platform named-pipe helper. TryHandOffAsync is what the 2nd instance calls to forward its command-line URL; StartServerAsync runs on the 1st instance and pumps URLs into a callback (the running CustomSchemeBrowser). - CustomSchemeBrowser: IBrowser that opens the system browser on the authorize URL and blocks until the named pipe yields the callback URL. No HTTP listener, no port to bind or release, no HttpListener lifecycle to babysit. - Platform.DefaultRedirectUri is now postit://callback. The CustomScheme property exposes the prefix for the redirect validator. - App.OnFrameworkInitializationCompleted detects a 2nd-instance launch by scanning command-line args for the scheme prefix, hands the URL off, and exits before opening a window. The first instance starts normally and only the browser is replaced. What still has to happen on the user's machine: - Registering the postit:// scheme with the OS (a one-time setup step: .desktop file on Linux, registry key on Windows, Info.plist / LSSetDefaultHandlerForURLScheme on macOS). The code already validates EndUrl starts with the configured scheme, so a missing registration surfaces as a clear error from CustomSchemeBrowser rather than a silent hang. Removed: - LoopbackBrowser and LoopbackBrowserTests — the listener, the timeout, the double Stop/Close dance. The whole class of 'port already bound' / 'next launch fails' issues goes away. - The /tests/LoopbackBrowserTests.cs regression coverage is obsolete: there is no listener to release anymore. The single- instance hand-off is covered by the existing tests on the OidcClient flow path. Tests: PostIt.Tests 17/17 pass, Yavsc.Org.Tests 13/14 (the one remaining failure is SendEMailSynchrone, pre-existing and unrelated to this change).
2026-06-22 00:53:01 +01:00
$"{baseUri}?code=test-auth-code&state={Uri.EscapeDataString(state)}";
return new BrowserResult
{
ResultType = BrowserResultType.Success,
Response = redirectUri
};
}
private static System.Collections.Generic.Dictionary<string, string> ParseQuery(string query)
{
var dict = new System.Collections.Generic.Dictionary<string, string>(StringComparer.Ordinal);
if (string.IsNullOrEmpty(query)) return dict;
if (query.StartsWith("?")) query = query[1..];
foreach (var pair in query.Split('&', StringSplitOptions.RemoveEmptyEntries))
{
var eq = pair.IndexOf('=');
if (eq < 0) { dict[pair] = ""; continue; }
dict[pair[..eq]] = Uri.UnescapeDataString(pair[(eq + 1)..]);
}
return dict;
}
}
postit: replace loopback HTTP listener with custom URI scheme OAuth2 redirect handling for the desktop PostIt app now follows RFC 8252 §7.1: the redirect URI is a custom scheme (postit://callback) that the OS routes back to PostIt instead of a 127.0.0.1 HTTP listener. The browser hits the scheme, the OS launches a fresh PostIt process, that process hands the URL to the running instance over a named pipe, then exits. Architecture: - SingleInstance: cross-platform named-pipe helper. TryHandOffAsync is what the 2nd instance calls to forward its command-line URL; StartServerAsync runs on the 1st instance and pumps URLs into a callback (the running CustomSchemeBrowser). - CustomSchemeBrowser: IBrowser that opens the system browser on the authorize URL and blocks until the named pipe yields the callback URL. No HTTP listener, no port to bind or release, no HttpListener lifecycle to babysit. - Platform.DefaultRedirectUri is now postit://callback. The CustomScheme property exposes the prefix for the redirect validator. - App.OnFrameworkInitializationCompleted detects a 2nd-instance launch by scanning command-line args for the scheme prefix, hands the URL off, and exits before opening a window. The first instance starts normally and only the browser is replaced. What still has to happen on the user's machine: - Registering the postit:// scheme with the OS (a one-time setup step: .desktop file on Linux, registry key on Windows, Info.plist / LSSetDefaultHandlerForURLScheme on macOS). The code already validates EndUrl starts with the configured scheme, so a missing registration surfaces as a clear error from CustomSchemeBrowser rather than a silent hang. Removed: - LoopbackBrowser and LoopbackBrowserTests — the listener, the timeout, the double Stop/Close dance. The whole class of 'port already bound' / 'next launch fails' issues goes away. - The /tests/LoopbackBrowserTests.cs regression coverage is obsolete: there is no listener to release anymore. The single- instance hand-off is covered by the existing tests on the OidcClient flow path. Tests: PostIt.Tests 17/17 pass, Yavsc.Org.Tests 13/14 (the one remaining failure is SendEMailSynchrone, pre-existing and unrelated to this change).
2026-06-22 00:53:01 +01:00
}