save progress
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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(" "));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user