save progress
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
using StellaOps.Authority.Plugin.Saml.Credentials;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Authority.Plugin.Saml.Tests.Credentials;
|
||||
|
||||
public sealed class SamlCredentialStoreTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildSessionCacheKey_IncludesPluginName()
|
||||
{
|
||||
var key = SamlCredentialStore.BuildSessionCacheKey("saml-test", "subject-1");
|
||||
|
||||
Assert.Equal("saml:saml-test:session:subject-1", key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Authority.Plugin.Saml;
|
||||
using StellaOps.Authority.Plugin.Saml.Claims;
|
||||
using StellaOps.Authority.Plugin.Saml.Credentials;
|
||||
using StellaOps.Authority.Plugins.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Authority.Plugin.Saml.Tests;
|
||||
|
||||
public sealed class SamlIdentityProviderPluginTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_ReturnsHealthy_WhenMetadataOk()
|
||||
{
|
||||
var plugin = CreatePlugin(HttpStatusCode.OK);
|
||||
|
||||
var result = await plugin.CheckHealthAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal(AuthorityPluginHealthStatus.Healthy, result.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckHealthAsync_ReturnsDegraded_WhenMetadataNotOk()
|
||||
{
|
||||
var plugin = CreatePlugin(HttpStatusCode.ServiceUnavailable);
|
||||
|
||||
var result = await plugin.CheckHealthAsync(CancellationToken.None);
|
||||
|
||||
Assert.Equal(AuthorityPluginHealthStatus.Degraded, result.Status);
|
||||
}
|
||||
|
||||
private static SamlIdentityProviderPlugin CreatePlugin(HttpStatusCode statusCode)
|
||||
{
|
||||
var pluginName = "saml-test";
|
||||
var options = new SamlPluginOptions
|
||||
{
|
||||
EntityId = "urn:stellaops:sp",
|
||||
IdpEntityId = "urn:idp:test",
|
||||
IdpMetadataUrl = "https://idp.example.com/metadata",
|
||||
ValidateSignature = false,
|
||||
SignAuthenticationRequests = false,
|
||||
SignLogoutRequests = false
|
||||
};
|
||||
|
||||
var optionsMonitor = new StaticOptionsMonitor(options, pluginName);
|
||||
var handler = new FixedResponseHandler(statusCode);
|
||||
var httpClientFactory = new TestHttpClientFactory(handler);
|
||||
var cache = new MemoryCache(new MemoryCacheOptions());
|
||||
|
||||
var credentialStore = new SamlCredentialStore(
|
||||
pluginName,
|
||||
optionsMonitor,
|
||||
cache,
|
||||
NullLogger<SamlCredentialStore>.Instance,
|
||||
httpClientFactory);
|
||||
|
||||
var claimsEnricher = new SamlClaimsEnricher(
|
||||
pluginName,
|
||||
optionsMonitor,
|
||||
NullLogger<SamlClaimsEnricher>.Instance);
|
||||
|
||||
var manifest = new AuthorityPluginManifest(
|
||||
Name: pluginName,
|
||||
Type: SamlPluginRegistrar.PluginType,
|
||||
Enabled: true,
|
||||
AssemblyName: null,
|
||||
AssemblyPath: null,
|
||||
Capabilities: new[] { AuthorityPluginCapabilities.Password },
|
||||
Metadata: new Dictionary<string, string?>(),
|
||||
ConfigPath: "saml.yaml");
|
||||
var context = new AuthorityPluginContext(manifest, new ConfigurationBuilder().Build());
|
||||
|
||||
return new SamlIdentityProviderPlugin(
|
||||
context,
|
||||
credentialStore,
|
||||
claimsEnricher,
|
||||
optionsMonitor,
|
||||
NullLogger<SamlIdentityProviderPlugin>.Instance,
|
||||
httpClientFactory);
|
||||
}
|
||||
|
||||
private sealed class FixedResponseHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly HttpStatusCode statusCode;
|
||||
|
||||
public FixedResponseHandler(HttpStatusCode statusCode)
|
||||
{
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(new HttpResponseMessage(statusCode)
|
||||
{
|
||||
Content = new StringContent("{}")
|
||||
});
|
||||
}
|
||||
|
||||
private sealed class TestHttpClientFactory : IHttpClientFactory
|
||||
{
|
||||
private readonly HttpMessageHandler handler;
|
||||
|
||||
public TestHttpClientFactory(HttpMessageHandler handler)
|
||||
{
|
||||
this.handler = handler;
|
||||
}
|
||||
|
||||
public HttpClient CreateClient(string name)
|
||||
=> new(handler, disposeHandler: false);
|
||||
}
|
||||
|
||||
private sealed class StaticOptionsMonitor : IOptionsMonitor<SamlPluginOptions>
|
||||
{
|
||||
private readonly SamlPluginOptions options;
|
||||
private readonly string pluginName;
|
||||
|
||||
public StaticOptionsMonitor(SamlPluginOptions options, string pluginName)
|
||||
{
|
||||
this.options = options;
|
||||
this.pluginName = pluginName;
|
||||
}
|
||||
|
||||
public SamlPluginOptions CurrentValue => options;
|
||||
|
||||
public SamlPluginOptions Get(string name)
|
||||
=> string.Equals(name, pluginName, StringComparison.Ordinal) ? options : options;
|
||||
|
||||
public IDisposable OnChange(Action<SamlPluginOptions, string> listener)
|
||||
=> new NoopDisposable();
|
||||
|
||||
private sealed class NoopDisposable : IDisposable
|
||||
{
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using StellaOps.Authority.Plugin.Saml;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Authority.Plugin.Saml.Tests;
|
||||
|
||||
public sealed class SamlMetadataParserTests
|
||||
{
|
||||
[Fact]
|
||||
public void TryExtractSigningCertificate_ReturnsCertificate()
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
var request = new CertificateRequest("CN=Test", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
|
||||
var notBefore = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
var cert = request.CreateSelfSigned(notBefore, notBefore.AddDays(30));
|
||||
var base64 = Convert.ToBase64String(cert.Export(X509ContentType.Cert));
|
||||
|
||||
var metadata = $"""
|
||||
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" entityID="urn:idp:test">
|
||||
<IDPSSODescriptor>
|
||||
<KeyDescriptor>
|
||||
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
|
||||
<X509Data>
|
||||
<X509Certificate>{base64}</X509Certificate>
|
||||
</X509Data>
|
||||
</KeyInfo>
|
||||
</KeyDescriptor>
|
||||
</IDPSSODescriptor>
|
||||
</EntityDescriptor>
|
||||
""";
|
||||
|
||||
var result = SamlMetadataParser.TryExtractSigningCertificate(metadata, out var extracted);
|
||||
|
||||
Assert.True(result);
|
||||
Assert.NotNull(extracted);
|
||||
Assert.Equal(cert.Thumbprint, extracted.Thumbprint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using StellaOps.Authority.Plugin.Saml;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Authority.Plugin.Saml.Tests;
|
||||
|
||||
public sealed class SamlPluginOptionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenEncryptedAssertionsEnabled()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
options.RequireEncryptedAssertions = true;
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => options.Validate());
|
||||
Assert.Contains("encrypted assertions", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenRequestSigningEnabled()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
options.SignAuthenticationRequests = true;
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => options.Validate());
|
||||
Assert.Contains("request signing", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenMetadataUrlNotHttps()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
options.IdpMetadataUrl = "http://idp.example.com/metadata";
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => options.Validate());
|
||||
Assert.Contains("metadata URL", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Throws_WhenMetadataTimeoutNonPositive()
|
||||
{
|
||||
var options = CreateOptions();
|
||||
options.MetadataTimeoutSeconds = 0;
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() => options.Validate());
|
||||
Assert.Contains("MetadataTimeoutSeconds", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static SamlPluginOptions CreateOptions()
|
||||
=> new()
|
||||
{
|
||||
EntityId = "urn:stellaops:sp",
|
||||
IdpEntityId = "urn:idp:test",
|
||||
IdpMetadataUrl = "https://idp.example.com/metadata",
|
||||
ValidateSignature = false,
|
||||
SignAuthenticationRequests = false,
|
||||
SignLogoutRequests = false
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user