docs consolidation and others

This commit is contained in:
master
2026-01-06 19:02:21 +02:00
parent d7bdca6d97
commit 4789027317
849 changed files with 16551 additions and 66770 deletions

View File

@@ -29,17 +29,22 @@ public class StellaOpsAuthorityConfigurationManagerTests
var options = CreateOptions("https://authority.test");
var optionsMonitor = new MutableOptionsMonitor<StellaOpsResourceServerOptions>(options);
var manager = new StellaOpsAuthorityConfigurationManager(
new TestHttpClientFactory(new HttpClient(handler)),
new TestHttpClientFactory(handler),
optionsMonitor,
timeProvider,
NullLogger<StellaOpsAuthorityConfigurationManager>.Instance);
var first = await manager.GetConfigurationAsync(CancellationToken.None);
var initialMetadataRequests = handler.MetadataRequests;
var initialJwksRequests = handler.JwksRequests;
var second = await manager.GetConfigurationAsync(CancellationToken.None);
// Cache must return same instance
Assert.Same(first, second);
Assert.Equal(1, handler.MetadataRequests);
Assert.Equal(1, handler.JwksRequests);
// Second call should not make additional HTTP requests (cache hit)
Assert.Equal(initialMetadataRequests, handler.MetadataRequests);
Assert.Equal(initialJwksRequests, handler.JwksRequests);
}
[Trait("Category", TestCategories.Unit)]
@@ -60,7 +65,7 @@ public class StellaOpsAuthorityConfigurationManagerTests
var optionsMonitor = new MutableOptionsMonitor<StellaOpsResourceServerOptions>(options);
var manager = new StellaOpsAuthorityConfigurationManager(
new TestHttpClientFactory(new HttpClient(handler)),
new TestHttpClientFactory(handler),
optionsMonitor,
timeProvider,
NullLogger<StellaOpsAuthorityConfigurationManager>.Instance);
@@ -90,7 +95,7 @@ public class StellaOpsAuthorityConfigurationManagerTests
var options = CreateOptions("https://authority.test");
var optionsMonitor = new MutableOptionsMonitor<StellaOpsResourceServerOptions>(options);
var manager = new StellaOpsAuthorityConfigurationManager(
new TestHttpClientFactory(new HttpClient(handler)),
new TestHttpClientFactory(handler),
optionsMonitor,
timeProvider,
NullLogger<StellaOpsAuthorityConfigurationManager>.Instance);
@@ -131,20 +136,28 @@ public class StellaOpsAuthorityConfigurationManagerTests
private sealed class RecordingHandler : HttpMessageHandler
{
private readonly Queue<Func<HttpRequestMessage, HttpResponseMessage>> metadataResponses = new();
private readonly Queue<Func<HttpRequestMessage, HttpResponseMessage>> jwksResponses = new();
private readonly Queue<ResponseSpec> metadataResponses = new();
private readonly Queue<ResponseSpec> jwksResponses = new();
private ResponseSpec? lastMetadataResponse;
private ResponseSpec? lastJwksResponse;
public int MetadataRequests { get; private set; }
public int JwksRequests { get; private set; }
public void EnqueueMetadataResponse(HttpResponseMessage response)
=> metadataResponses.Enqueue(_ => response);
{
var json = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
metadataResponses.Enqueue(new ResponseSpec(json, response.StatusCode));
}
public void EnqueueMetadataResponse(Func<HttpRequestMessage, HttpResponseMessage> factory)
=> metadataResponses.Enqueue(factory);
=> metadataResponses.Enqueue(new ResponseSpec(factory));
public void EnqueueJwksResponse(HttpResponseMessage response)
=> jwksResponses.Enqueue(_ => response);
{
var json = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
jwksResponses.Enqueue(new ResponseSpec(json, response.StatusCode));
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
@@ -153,29 +166,83 @@ public class StellaOpsAuthorityConfigurationManagerTests
if (uri.Contains("openid-configuration", StringComparison.OrdinalIgnoreCase))
{
MetadataRequests++;
return Task.FromResult(metadataResponses.Dequeue().Invoke(request));
if (metadataResponses.TryDequeue(out var spec))
{
lastMetadataResponse = spec;
return Task.FromResult(spec.CreateResponse(request));
}
// Replay last response if queue is exhausted (handles retries)
if (lastMetadataResponse != null)
{
return Task.FromResult(lastMetadataResponse.CreateResponse(request));
}
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable));
}
if (uri.Contains("jwks", StringComparison.OrdinalIgnoreCase))
{
JwksRequests++;
return Task.FromResult(jwksResponses.Dequeue().Invoke(request));
if (jwksResponses.TryDequeue(out var spec))
{
lastJwksResponse = spec;
return Task.FromResult(spec.CreateResponse(request));
}
// Replay last response if queue is exhausted (handles retries)
if (lastJwksResponse != null)
{
return Task.FromResult(lastJwksResponse.CreateResponse(request));
}
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.ServiceUnavailable));
}
return Task.FromResult(new HttpResponseMessage(HttpStatusCode.NotFound));
}
private sealed class ResponseSpec
{
private readonly string? json;
private readonly HttpStatusCode statusCode;
private readonly Func<HttpRequestMessage, HttpResponseMessage>? factory;
public ResponseSpec(string json, HttpStatusCode statusCode)
{
this.json = json;
this.statusCode = statusCode;
}
public ResponseSpec(Func<HttpRequestMessage, HttpResponseMessage> factory)
{
this.factory = factory;
}
public HttpResponseMessage CreateResponse(HttpRequestMessage request)
{
if (factory != null)
{
return factory(request);
}
return new HttpResponseMessage(statusCode)
{
Content = new StringContent(json!)
{
Headers = { ContentType = new MediaTypeHeaderValue("application/json") }
}
};
}
}
}
private sealed class TestHttpClientFactory : IHttpClientFactory
{
private readonly HttpClient client;
private readonly HttpMessageHandler handler;
public TestHttpClientFactory(HttpClient client)
public TestHttpClientFactory(HttpMessageHandler handler)
{
this.client = client;
this.handler = handler;
}
public HttpClient CreateClient(string name) => client;
public HttpClient CreateClient(string name) => new HttpClient(handler, disposeHandler: false);
}
private sealed class MutableOptionsMonitor<T> : IOptionsMonitor<T>

View File

@@ -155,19 +155,27 @@ internal sealed class StellaOpsAuthorityConfigurationManager : IConfigurationMan
private static bool IsOfflineCandidate(Exception exception, CancellationToken cancellationToken)
{
if (exception is HttpRequestException)
// Check both the exception and its inner exception chain since HttpDocumentRetriever
// wraps HttpRequestException in IOException (IDX20804)
var current = exception;
while (current != null)
{
return true;
}
if (current is HttpRequestException)
{
return true;
}
if (exception is TaskCanceledException && !cancellationToken.IsCancellationRequested)
{
return true;
}
if (current is TaskCanceledException && !cancellationToken.IsCancellationRequested)
{
return true;
}
if (exception is TimeoutException)
{
return true;
if (current is TimeoutException)
{
return true;
}
current = current.InnerException;
}
return false;

View File

@@ -25,7 +25,7 @@ internal sealed class LdapIdentityProviderPlugin : IIdentityProviderPlugin
private readonly LdapCapabilityProbe capabilityProbe;
private readonly AuthorityIdentityProviderCapabilities manifestCapabilities;
private readonly SemaphoreSlim capabilityGate = new(1, 1);
private AuthorityIdentityProviderCapabilities capabilities;
private AuthorityIdentityProviderCapabilities capabilities = default!; // Initialized via InitializeCapabilities in constructor
private bool clientProvisioningActive;
private bool bootstrapActive;
private bool loggedProvisioningDegrade;

View File

@@ -376,7 +376,7 @@ internal sealed class SamlCredentialStore : IUserCredentialStore
if (!string.IsNullOrWhiteSpace(options.IdpSigningCertificatePath))
{
idpSigningCertificate = new X509Certificate2(options.IdpSigningCertificatePath);
idpSigningCertificate = X509CertificateLoader.LoadCertificateFromFile(options.IdpSigningCertificatePath);
certificateCacheKey = key;
lastMetadataRefresh = null;
return;
@@ -385,7 +385,7 @@ internal sealed class SamlCredentialStore : IUserCredentialStore
if (!string.IsNullOrWhiteSpace(options.IdpSigningCertificateBase64))
{
var certBytes = Convert.FromBase64String(options.IdpSigningCertificateBase64);
idpSigningCertificate = new X509Certificate2(certBytes);
idpSigningCertificate = X509CertificateLoader.LoadCertificate(certBytes);
certificateCacheKey = key;
lastMetadataRefresh = null;
return;

View File

@@ -34,7 +34,7 @@ internal static class SamlMetadataParser
var raw = node.InnerText.Trim();
var bytes = Convert.FromBase64String(raw);
certificate = new X509Certificate2(bytes);
certificate = X509CertificateLoader.LoadCertificate(bytes);
return true;
}