docs consolidation and others
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user