Identity reloaded
This commit is contained in:
parent
ef0f5ddcac
commit
333b066e66
33 changed files with 195 additions and 180 deletions
|
|
@ -10,7 +10,7 @@ namespace PostIt.Tests;
|
|||
/// URL emitted by OidcClient, extracts its <c>state</c>, and returns a
|
||||
/// BrowserResult that mimics the OIDC redirect-with-code callback.
|
||||
///
|
||||
/// The paired <see cref="OidcStubAuthority"/>'s token endpoint accepts
|
||||
/// 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
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ public class LoginPageViewModelTests
|
|||
// short-circuits the system browser. The authority signs its
|
||||
// access_token with RS256; the fake browser captures the redirect
|
||||
// URI so the authority can complete the token exchange.
|
||||
using var authority = await OidcStubAuthority.StartAsync();
|
||||
using var authority = await OIDCStubAuthority.StartAsync();
|
||||
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
|
||||
|
||||
var settings = new PostIt.Settings
|
||||
|
|
@ -96,7 +96,7 @@ public class LoginPageViewModelTests
|
|||
// double slash before /.well-known/openid-configuration. The
|
||||
// stub advertises itself without the trailing slash; OidcClient
|
||||
// must bridge.
|
||||
using var authority = await OidcStubAuthority.StartAsync();
|
||||
using var authority = await OIDCStubAuthority.StartAsync();
|
||||
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
|
||||
|
||||
var settings = new PostIt.Settings
|
||||
|
|
@ -254,4 +254,4 @@ public class LoginPageViewModelTests
|
|||
"https://yavsc.example.com/.well-known/openid-configuration",
|
||||
vm.StatusMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace PostIt.Tests;
|
|||
/// the browser intercepts the authorize redirect, the server completes
|
||||
/// the token exchange.
|
||||
/// </summary>
|
||||
public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
|
||||
public sealed class OIDCStubAuthority : IAsyncDisposable, IDisposable
|
||||
{
|
||||
private readonly HttpListener _listener;
|
||||
private readonly RSA _rsa;
|
||||
|
|
@ -30,7 +30,7 @@ public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
|
|||
public string Issuer { get; }
|
||||
public string LoopbackRedirectUri { get; }
|
||||
|
||||
private OidcStubAuthority(HttpListener listener, RSA rsa, string kid, string issuer, string loopback)
|
||||
private OIDCStubAuthority(HttpListener listener, RSA rsa, string kid, string issuer, string loopback)
|
||||
{
|
||||
_listener = listener;
|
||||
_rsa = rsa;
|
||||
|
|
@ -39,7 +39,7 @@ public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
|
|||
LoopbackRedirectUri = loopback;
|
||||
}
|
||||
|
||||
public static async Task<OidcStubAuthority> StartAsync()
|
||||
public static async Task<OIDCStubAuthority> StartAsync()
|
||||
{
|
||||
// Pick a free loopback port.
|
||||
var port = GetFreePort();
|
||||
|
|
@ -53,7 +53,7 @@ public sealed class OidcStubAuthority : IAsyncDisposable, IDisposable
|
|||
var rsa = RSA.Create(2048);
|
||||
var kid = "test-key-1";
|
||||
|
||||
var authority = new OidcStubAuthority(listener, rsa, kid, prefix.TrimEnd('/'), loopback);
|
||||
var authority = new OIDCStubAuthority(listener, rsa, kid, prefix.TrimEnd('/'), loopback);
|
||||
_ = Task.Run(() => authority.AcceptLoopAsync(authority._cts.Token));
|
||||
return authority;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ namespace PostIt.Tests;
|
|||
/// End-to-end coverage of <see cref="YavscApiClient"/>: silent
|
||||
/// refresh on a near-expiry access token, 401-driven refresh + retry,
|
||||
/// and persistence of the token bundle via <see cref="TokenStore"/>.
|
||||
/// Uses the project's <see cref="OidcStubAuthority"/> for the IdP and
|
||||
/// Uses the project's <see cref="OIDCStubAuthority"/> for the IdP and
|
||||
/// a tiny in-process HTTP listener for the API server side.
|
||||
/// </summary>
|
||||
public class YavscApiClientTests
|
||||
|
|
@ -45,7 +45,7 @@ public class YavscApiClientTests
|
|||
// in-memory access token as expired and re-run a call. The
|
||||
// refresh path must rotate the refresh token transparently
|
||||
// and the API call must succeed with the new token.
|
||||
using var authority = await OidcStubAuthority.StartAsync();
|
||||
using var authority = await OIDCStubAuthority.StartAsync();
|
||||
using var apiServer = new StubApiServer();
|
||||
await apiServer.StartAsync();
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ public class YavscApiClientTests
|
|||
var reloaded = new YavscApiClient(settings, new TokenStore(tokensPath));
|
||||
|
||||
var posts = await reloaded.CallAsync<List<StubApiServer.Post>>(
|
||||
HttpMethod.Get, "posts");
|
||||
HttpMethod.Get, "posts", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotNull(posts);
|
||||
Assert.NotEmpty(posts);
|
||||
|
|
@ -85,7 +85,7 @@ public class YavscApiClientTests
|
|||
{
|
||||
// API server returns 401 on the first request, 200 on the next.
|
||||
// YavscApiClient must refresh, then retry exactly once.
|
||||
using var authority = await OidcStubAuthority.StartAsync();
|
||||
using var authority = await OIDCStubAuthority.StartAsync();
|
||||
using var apiServer = new StubApiServer(forceFirstRequest: true);
|
||||
await apiServer.StartAsync();
|
||||
|
||||
|
|
@ -97,7 +97,7 @@ public class YavscApiClientTests
|
|||
settings, authority, tokensPath);
|
||||
|
||||
var posts = await client.CallAsync<List<StubApiServer.Post>>(
|
||||
HttpMethod.Get, "posts");
|
||||
HttpMethod.Get, "posts", TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.NotEmpty(posts);
|
||||
Assert.Equal(2, apiServer.RequestCount);
|
||||
|
|
@ -125,14 +125,15 @@ public class YavscApiClientTests
|
|||
var client = new YavscApiClient(settings, new TokenStore(Path.Combine(
|
||||
Path.GetTempPath(), $"postit-tests-noop-{Guid.NewGuid():N}.json")));
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
client.CallAsync<JsonElement>(HttpMethod.Get, "posts"));
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() =>
|
||||
client.CallAsync<JsonElement>(HttpMethod.Get, "posts", TestContext.Current.CancellationToken));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HasValidSession_is_true_after_login()
|
||||
{
|
||||
using var authority = await OidcStubAuthority.StartAsync();
|
||||
using var authority = await OIDCStubAuthority.StartAsync();
|
||||
using var apiServer = new StubApiServer();
|
||||
await apiServer.StartAsync();
|
||||
|
||||
|
|
@ -154,7 +155,7 @@ public class YavscApiClientTests
|
|||
|
||||
// --- helpers --------------------------------------------------------
|
||||
|
||||
private static PostIt.Settings BuildSettings(OidcStubAuthority authority, string apiBaseUrl) => new()
|
||||
private static PostIt.Settings BuildSettings(OIDCStubAuthority authority, string apiBaseUrl) => new()
|
||||
{
|
||||
Authentication = new AuthenticationSettings
|
||||
{
|
||||
|
|
@ -167,7 +168,7 @@ public class YavscApiClientTests
|
|||
};
|
||||
|
||||
private static async Task<YavscApiClient> LoginAndPersistAsync(
|
||||
PostIt.Settings settings, OidcStubAuthority authority, string tokensPath)
|
||||
PostIt.Settings settings, OIDCStubAuthority authority, string tokensPath)
|
||||
{
|
||||
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
|
||||
var client = new YavscApiClient(settings, new TokenStore(tokensPath));
|
||||
|
|
@ -181,7 +182,7 @@ public class YavscApiClientTests
|
|||
/// <summary>
|
||||
/// YavscApiClient.LoginInteractiveAsync delegates to
|
||||
/// Platform.CreateBrowser. We can't override that static cleanly
|
||||
/// from xunit.v3, so we rebuild the call by re-routing the
|
||||
/// from XUnit.v3, so we rebuild the call by re-routing the
|
||||
/// Platform.CreateBrowser delegate for the duration of the call.
|
||||
/// </summary>
|
||||
private static async Task LoginWithBrowserAsync(
|
||||
|
|
@ -214,7 +215,7 @@ public class YavscApiClientTests
|
|||
File.WriteAllText(tokensPath, JsonSerializer.Serialize(record));
|
||||
}
|
||||
|
||||
// --- OidcLoginPhase progress tests ---------------------------------
|
||||
// --- OIDCLoginPhase progress tests ---------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Collecting Progress<T> is documented to capture reports
|
||||
|
|
@ -225,7 +226,7 @@ public class YavscApiClientTests
|
|||
[Fact]
|
||||
public async Task LoginInteractiveAsync_reports_Discovering_then_Success()
|
||||
{
|
||||
using var authority = await OidcStubAuthority.StartAsync();
|
||||
using var authority = await OIDCStubAuthority.StartAsync();
|
||||
using var apiServer = new StubApiServer();
|
||||
await apiServer.StartAsync();
|
||||
|
||||
|
|
@ -234,18 +235,18 @@ public class YavscApiClientTests
|
|||
var client = new YavscApiClient(settings, new TokenStore(tokensPath));
|
||||
var browser = new FakeAuthorizingBrowser(authority.LoopbackRedirectUri);
|
||||
|
||||
var reported = new System.Collections.Generic.List<OidcLoginPhase>();
|
||||
var progress = new SyncProgress<OidcLoginPhase>(reported);
|
||||
var reported = new System.Collections.Generic.List<OIDCLoginPhase>();
|
||||
var progress = new SyncProgress<OIDCLoginPhase>(reported);
|
||||
|
||||
try
|
||||
{
|
||||
await LoginWithBrowserAsync(client, browser.CreateBrowser(), progress);
|
||||
// SyncProgress captures reports synchronously — no flush needed.
|
||||
|
||||
Assert.Contains(OidcLoginPhase.Discovering, reported);
|
||||
Assert.Contains(OidcLoginPhase.OpeningBrowser, reported);
|
||||
Assert.Contains(OidcLoginPhase.ExchangingCode, reported);
|
||||
Assert.Equal(OidcLoginPhase.Success, Last(reported));
|
||||
Assert.Contains(OIDCLoginPhase.Discovering, reported);
|
||||
Assert.Contains(OIDCLoginPhase.OpeningBrowser, reported);
|
||||
Assert.Contains(OIDCLoginPhase.ExchangingCode, reported);
|
||||
Assert.Equal(OIDCLoginPhase.Success, Last(reported));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -256,24 +257,24 @@ public class YavscApiClientTests
|
|||
[Fact]
|
||||
public async Task LoginInteractiveAsync_reports_Error_when_browser_missing()
|
||||
{
|
||||
using var authority = await OidcStubAuthority.StartAsync();
|
||||
using var authority = await OIDCStubAuthority.StartAsync();
|
||||
using var apiServer = new StubApiServer();
|
||||
await apiServer.StartAsync();
|
||||
|
||||
var settings = BuildSettings(authority, apiServer.BaseUrl);
|
||||
var client = new YavscApiClient(settings, new TokenStore(TokensPath()));
|
||||
var reported = new System.Collections.Generic.List<OidcLoginPhase>();
|
||||
var progress = new SyncProgress<OidcLoginPhase>(reported);
|
||||
var reported = new System.Collections.Generic.List<OIDCLoginPhase>();
|
||||
var progress = new SyncProgress<OIDCLoginPhase>(reported);
|
||||
|
||||
var original = Platform.CreateBrowser;
|
||||
try
|
||||
{
|
||||
Platform.CreateBrowser = () => null; // simulate no browser wired up
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => client.LoginInteractiveAsync(progress));
|
||||
() => client.LoginInteractiveAsync(progress, TestContext.Current.CancellationToken));
|
||||
// SyncProgress captures reports synchronously — no flush needed.
|
||||
|
||||
Assert.Equal(OidcLoginPhase.Error, Last(reported));
|
||||
Assert.Equal(OIDCLoginPhase.Error, Last(reported));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -284,7 +285,7 @@ public class YavscApiClientTests
|
|||
[Fact]
|
||||
public async Task TrySilentLoginAsync_returns_false_when_no_bundle_on_disk()
|
||||
{
|
||||
using var authority = await OidcStubAuthority.StartAsync();
|
||||
using var authority = await OIDCStubAuthority.StartAsync();
|
||||
using var apiServer = new StubApiServer();
|
||||
await apiServer.StartAsync();
|
||||
|
||||
|
|
@ -293,7 +294,7 @@ public class YavscApiClientTests
|
|||
// Tokens file deliberately doesn't exist.
|
||||
var client = new YavscApiClient(settings, new TokenStore(tokensPath));
|
||||
|
||||
var ok = await client.TrySilentLoginAsync();
|
||||
var ok = await client.TrySilentLoginAsync(null, TestContext.Current.CancellationToken);
|
||||
Assert.False(ok);
|
||||
Assert.False(client.HasValidSession);
|
||||
}
|
||||
|
|
@ -301,7 +302,7 @@ public class YavscApiClientTests
|
|||
[Fact]
|
||||
public async Task TrySilentLoginAsync_returns_true_when_access_token_still_valid()
|
||||
{
|
||||
using var authority = await OidcStubAuthority.StartAsync();
|
||||
using var authority = await OIDCStubAuthority.StartAsync();
|
||||
using var apiServer = new StubApiServer();
|
||||
await apiServer.StartAsync();
|
||||
|
||||
|
|
@ -314,7 +315,7 @@ public class YavscApiClientTests
|
|||
{
|
||||
await LoginWithBrowserAsync(client, browser.CreateBrowser());
|
||||
// Login fresh → access token is far from expiry.
|
||||
var ok = await client.TrySilentLoginAsync();
|
||||
var ok = await client.TrySilentLoginAsync(null, TestContext.Current.CancellationToken);
|
||||
Assert.True(ok);
|
||||
Assert.True(client.HasValidSession);
|
||||
}
|
||||
|
|
@ -327,7 +328,7 @@ public class YavscApiClientTests
|
|||
[Fact]
|
||||
public async Task TrySilentLoginAsync_returns_true_when_refresh_succeeds()
|
||||
{
|
||||
using var authority = await OidcStubAuthority.StartAsync();
|
||||
using var authority = await OIDCStubAuthority.StartAsync();
|
||||
using var apiServer = new StubApiServer();
|
||||
await apiServer.StartAsync();
|
||||
|
||||
|
|
@ -351,13 +352,13 @@ public class YavscApiClientTests
|
|||
// matches the disk: access expired, refresh still good.
|
||||
var client = new YavscApiClient(settings, store);
|
||||
|
||||
var reported = new System.Collections.Generic.List<OidcLoginPhase>();
|
||||
var progress = new SyncProgress<OidcLoginPhase>(reported);
|
||||
var reported = new System.Collections.Generic.List<OIDCLoginPhase>();
|
||||
var progress = new SyncProgress<OIDCLoginPhase>(reported);
|
||||
|
||||
var ok = await client.TrySilentLoginAsync(progress);
|
||||
var ok = await client.TrySilentLoginAsync(progress, TestContext.Current.CancellationToken);
|
||||
Assert.True(ok, "silent refresh should succeed via the stub authority.");
|
||||
Assert.Contains(OidcLoginPhase.ExchangingCode, reported);
|
||||
Assert.Equal(OidcLoginPhase.Success, Last(reported));
|
||||
Assert.Contains(OIDCLoginPhase.ExchangingCode, reported);
|
||||
Assert.Equal(OIDCLoginPhase.Success, Last(reported));
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -430,7 +431,7 @@ public class YavscApiClientTests
|
|||
/// overload stays for tests that don't care about phase events.
|
||||
/// </summary>
|
||||
private static async Task LoginWithBrowserAsync(
|
||||
YavscApiClient client, IBrowser browser, IProgress<OidcLoginPhase>? progress = null)
|
||||
YavscApiClient client, IBrowser browser, IProgress<OIDCLoginPhase>? progress = null)
|
||||
{
|
||||
var original = Platform.CreateBrowser;
|
||||
try
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue