save progress

This commit is contained in:
StellaOps Bot
2026-01-03 00:47:24 +02:00
parent 3f197814c5
commit ca578801fd
319 changed files with 32478 additions and 2202 deletions

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using StellaOps.Authority.Plugins.Abstractions;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Authority.Plugins.Abstractions.Tests;
public class AuthorityClientDescriptorNormalizationTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ClientDescriptor_NormalizesScopesAndMetadata()
{
var descriptor = new AuthorityClientDescriptor(
clientId: "client-1",
displayName: "Client 1",
confidential: true,
allowedGrantTypes: new[] { "client_credentials", " client_credentials " },
allowedScopes: new[] { " Authority.Users.Read ", "authority.users.read" },
allowedAudiences: new[] { "api", " api " },
properties: new Dictionary<string, string?>
{
[AuthorityClientMetadataKeys.Tenant] = " Tenant-A ",
[AuthorityClientMetadataKeys.Project] = " Project-One "
});
Assert.Equal("tenant-a", descriptor.Tenant);
Assert.Equal("project-one", descriptor.Project);
Assert.Single(descriptor.AllowedGrantTypes);
Assert.Single(descriptor.AllowedAudiences);
Assert.Contains("authority.users.read", descriptor.AllowedScopes);
Assert.Equal("project-one", descriptor.Properties[AuthorityClientMetadataKeys.Project]);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void CertificateBindingRegistration_NormalizesFields()
{
var binding = new AuthorityClientCertificateBindingRegistration(
thumbprint: "aa:bb:cc:dd",
serialNumber: " 01ff ",
subject: " CN=test ",
issuer: " CN=issuer ",
subjectAlternativeNames: new[] { "EXAMPLE.com", " example.com ", "spiffe://client" },
label: " primary ");
Assert.Equal("AABBCCDD", binding.Thumbprint);
Assert.Equal("01ff", binding.SerialNumber);
Assert.Equal("CN=test", binding.Subject);
Assert.Equal("CN=issuer", binding.Issuer);
Assert.Equal("primary", binding.Label);
Assert.Equal(2, binding.SubjectAlternativeNames.Count);
}
}

View File

@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using StellaOps.Authority.Plugins.Abstractions;
using Microsoft.Extensions.Configuration;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Authority.Plugins.Abstractions.Tests;
public class AuthorityIdentityProviderHandleTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Dispose_DisposesScope()
{
var scope = new TrackingScope();
var handle = CreateHandle(scope);
handle.Dispose();
Assert.Equal(1, scope.DisposeCalls);
Assert.Equal(0, scope.DisposeAsyncCalls);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task DisposeAsync_DisposesScopeAsync()
{
var scope = new TrackingScope();
var handle = CreateHandle(scope);
await handle.DisposeAsync();
Assert.Equal(0, scope.DisposeCalls);
Assert.Equal(1, scope.DisposeAsyncCalls);
}
private static AuthorityIdentityProviderHandle CreateHandle(TrackingScope scope)
{
var asyncScope = new AsyncServiceScope(scope);
var metadata = new AuthorityIdentityProviderMetadata(
"standard",
"standard",
new AuthorityIdentityProviderCapabilities(true, false, false, false));
var plugin = new StubIdentityProviderPlugin();
return new AuthorityIdentityProviderHandle(asyncScope, metadata, plugin);
}
private sealed class TrackingScope : IServiceScope, IAsyncDisposable
{
public IServiceProvider ServiceProvider { get; } = new ServiceCollection().BuildServiceProvider();
public int DisposeCalls { get; private set; }
public int DisposeAsyncCalls { get; private set; }
public void Dispose()
{
DisposeCalls++;
}
public ValueTask DisposeAsync()
{
DisposeAsyncCalls++;
return ValueTask.CompletedTask;
}
}
private sealed class StubIdentityProviderPlugin : IIdentityProviderPlugin
{
public string Name => "standard";
public string Type => "standard";
public AuthorityPluginContext Context { get; } = new(
new AuthorityPluginManifest(
"standard",
"standard",
true,
"assembly",
"path",
Array.Empty<string>(),
new Dictionary<string, string?>(),
"standard.yaml"),
new ConfigurationBuilder().Build());
public IUserCredentialStore Credentials { get; } = new StubCredentialStore();
public IClaimsEnricher ClaimsEnricher { get; } = new StubClaimsEnricher();
public IClientProvisioningStore? ClientProvisioning => null;
public AuthorityIdentityProviderCapabilities Capabilities { get; } = new(true, false, false, false);
public ValueTask<AuthorityPluginHealthResult> CheckHealthAsync(CancellationToken cancellationToken)
=> ValueTask.FromResult(AuthorityPluginHealthResult.Healthy());
}
private sealed class StubCredentialStore : IUserCredentialStore
{
public ValueTask<AuthorityCredentialVerificationResult> VerifyPasswordAsync(string username, string password, CancellationToken cancellationToken)
=> ValueTask.FromResult(AuthorityCredentialVerificationResult.Failure(AuthorityCredentialFailureCode.InvalidCredentials));
public ValueTask<AuthorityPluginOperationResult<AuthorityUserDescriptor>> UpsertUserAsync(AuthorityUserRegistration registration, CancellationToken cancellationToken)
=> ValueTask.FromResult(AuthorityPluginOperationResult<AuthorityUserDescriptor>.Failure("not_supported"));
public ValueTask<AuthorityUserDescriptor?> FindBySubjectAsync(string subjectId, CancellationToken cancellationToken)
=> ValueTask.FromResult<AuthorityUserDescriptor?>(null);
}
private sealed class StubClaimsEnricher : IClaimsEnricher
{
public ValueTask EnrichAsync(ClaimsIdentity identity, AuthorityClaimsEnrichmentContext context, CancellationToken cancellationToken)
=> ValueTask.CompletedTask;
}
}

View File

@@ -0,0 +1,45 @@
using System.Collections.Generic;
using StellaOps.Authority.Plugins.Abstractions;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Authority.Plugins.Abstractions.Tests;
public class AuthorityPluginManifestTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void HasCapability_IgnoresCaseAndWhitespace()
{
var manifest = new AuthorityPluginManifest(
"standard",
"standard",
true,
"assembly",
"path",
new List<string> { " password ", "Bootstrap" },
new Dictionary<string, string?>(),
"config.yaml");
Assert.True(manifest.HasCapability("password"));
Assert.True(manifest.HasCapability(" bootstrap "));
Assert.False(manifest.HasCapability("mfa"));
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void HasCapability_ReturnsFalse_ForBlankInput()
{
var manifest = new AuthorityPluginManifest(
"standard",
"standard",
true,
"assembly",
"path",
new List<string>(),
new Dictionary<string, string?>(),
"config.yaml");
Assert.False(manifest.HasCapability(" "));
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using StellaOps.Authority.Plugins.Abstractions;
using StellaOps.Cryptography;
using Xunit;
using StellaOps.TestKit;
namespace StellaOps.Authority.Plugins.Abstractions.Tests;
public class AuthoritySecretHasherTests
{
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHash_Throws_WhenUnconfiguredAlgorithmRequested()
{
AuthoritySecretHasher.Reset();
var ex = Assert.Throws<InvalidOperationException>(() => AuthoritySecretHasher.ComputeHash("secret", "sha512"));
Assert.Contains("not configured", ex.Message, StringComparison.OrdinalIgnoreCase);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHash_UsesConfiguredDefaultAlgorithm()
{
using var scope = AuthoritySecretHasher.BeginScope(new FakeCryptoHash(), "sha512");
var hash = AuthoritySecretHasher.ComputeHash("secret");
Assert.Equal(Convert.ToBase64String("SHA512"u8.ToArray()), hash);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ComputeHash_UsesExplicitAlgorithmWhenProvided()
{
using var scope = AuthoritySecretHasher.BeginScope(new FakeCryptoHash(), "sha256");
var hash = AuthoritySecretHasher.ComputeHash("secret", "sha384");
Assert.Equal(Convert.ToBase64String("SHA384"u8.ToArray()), hash);
}
private sealed class FakeCryptoHash : ICryptoHash
{
public byte[] ComputeHash(ReadOnlySpan<byte> data, string? algorithmId = null)
=> System.Text.Encoding.UTF8.GetBytes(algorithmId ?? string.Empty);
public string ComputeHashHex(ReadOnlySpan<byte> data, string? algorithmId = null)
=> throw new NotImplementedException();
public string ComputeHashBase64(ReadOnlySpan<byte> data, string? algorithmId = null)
=> throw new NotImplementedException();
public ValueTask<byte[]> ComputeHashAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public ValueTask<string> ComputeHashHexAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public byte[] ComputeHashForPurpose(ReadOnlySpan<byte> data, string purpose)
=> throw new NotImplementedException();
public string ComputeHashHexForPurpose(ReadOnlySpan<byte> data, string purpose)
=> throw new NotImplementedException();
public string ComputeHashBase64ForPurpose(ReadOnlySpan<byte> data, string purpose)
=> throw new NotImplementedException();
public ValueTask<byte[]> ComputeHashForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public ValueTask<string> ComputeHashHexForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public string GetAlgorithmForPurpose(string purpose)
=> throw new NotImplementedException();
public string GetHashPrefix(string purpose)
=> throw new NotImplementedException();
public string ComputePrefixedHashForPurpose(ReadOnlySpan<byte> data, string purpose)
=> throw new NotImplementedException();
}
}