fix: dead jobengine route path rewriting + legacy endpoint delegation

- Fix PacksRegistry route: rewrite /jobengine/registry/packs → /packs on target
- Fix first-signal route: delegate to real handler instead of 501 stub
- Release-orchestrator persistence extraction progress

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-04-08 18:18:26 +03:00
parent 2d83ca08b8
commit e1f5341c82
17 changed files with 1272 additions and 25 deletions

View File

@@ -0,0 +1,410 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Cryptography;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.Cryptography.Tests;
public class TenantAwareCryptoProviderRegistryTests
{
private const string TenantA = "tenant-a";
private const string TenantB = "tenant-b";
private static readonly ILogger Logger = NullLoggerFactory.Instance.CreateLogger<TenantAwareCryptoProviderRegistry>();
// ---------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------
private sealed class FakePreferenceProvider : ITenantCryptoPreferenceProvider
{
private readonly Dictionary<string, IReadOnlyList<string>> preferences = new(StringComparer.OrdinalIgnoreCase);
public int CallCount { get; private set; }
public void Set(string tenantId, string scope, IReadOnlyList<string> providers)
{
preferences[$"{tenantId}:{scope}"] = providers;
}
public Task<IReadOnlyList<string>> GetPreferredProvidersAsync(
string tenantId, string algorithmScope = "*", CancellationToken cancellationToken = default)
{
CallCount++;
var key = $"{tenantId}:{algorithmScope}";
return Task.FromResult(
preferences.TryGetValue(key, out var list) ? list : (IReadOnlyList<string>)Array.Empty<string>());
}
}
private sealed class FakeProvider : ICryptoProvider
{
private readonly HashSet<(CryptoCapability, string)> supported = new();
public FakeProvider(string name) => Name = name;
public string Name { get; }
public FakeProvider WithSupport(CryptoCapability capability, string algorithm)
{
supported.Add((capability, algorithm));
return this;
}
public bool Supports(CryptoCapability capability, string algorithmId) => supported.Contains((capability, algorithmId));
public IPasswordHasher GetPasswordHasher(string algorithmId) => throw new NotSupportedException();
public ICryptoHasher GetHasher(string algorithmId) => new StubHasher(algorithmId);
public ICryptoSigner GetSigner(string algorithmId, CryptoKeyReference keyReference)
=> new StubSigner(Name, keyReference.KeyId, algorithmId);
public void UpsertSigningKey(CryptoSigningKey signingKey) { }
public bool RemoveSigningKey(string keyId) => false;
public IReadOnlyCollection<CryptoSigningKey> GetSigningKeys() => Array.Empty<CryptoSigningKey>();
}
private sealed class StubHasher : ICryptoHasher
{
public StubHasher(string algorithmId) => AlgorithmId = algorithmId;
public string AlgorithmId { get; }
public byte[] ComputeHash(ReadOnlySpan<byte> data) => Array.Empty<byte>();
public string ComputeHashHex(ReadOnlySpan<byte> data) => "";
}
private sealed class StubSigner : ICryptoSigner
{
public StubSigner(string provider, string keyId, string algorithmId)
{
Provider = provider;
KeyId = keyId;
AlgorithmId = algorithmId;
}
public string Provider { get; }
public string KeyId { get; }
public string AlgorithmId { get; }
public ValueTask<byte[]> SignAsync(ReadOnlyMemory<byte> data, CancellationToken ct = default)
=> ValueTask.FromResult(Array.Empty<byte>());
public ValueTask<bool> VerifyAsync(ReadOnlyMemory<byte> data, ReadOnlyMemory<byte> signature, CancellationToken ct = default)
=> ValueTask.FromResult(true);
public Microsoft.IdentityModel.Tokens.JsonWebKey ExportPublicJsonWebKey() => new();
}
private static CryptoProviderRegistry BuildInnerRegistry(params FakeProvider[] providers)
{
return new CryptoProviderRegistry(providers);
}
private static TenantAwareCryptoProviderRegistry Build(
ICryptoProviderRegistry inner,
FakePreferenceProvider preferences,
Func<string?> tenantAccessor,
TimeSpan? cacheTtl = null)
{
return new TenantAwareCryptoProviderRegistry(
inner,
preferences,
tenantAccessor,
TimeProvider.System,
Logger,
cacheTtl ?? TimeSpan.FromMinutes(5));
}
// ---------------------------------------------------------------
// ResolveOrThrow
// ---------------------------------------------------------------
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveOrThrow_NoTenantContext_FallsBackToInnerRegistry()
{
var providerA = new FakeProvider("providerA").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var providerB = new FakeProvider("providerB").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var inner = BuildInnerRegistry(providerA, providerB);
var prefs = new FakePreferenceProvider();
var registry = Build(inner, prefs, () => null);
var resolved = registry.ResolveOrThrow(CryptoCapability.Signing, SignatureAlgorithms.Es256);
// No tenant context => inner registry default ordering => providerA (first registered)
Assert.Same(providerA, resolved);
Assert.Equal(0, prefs.CallCount); // Should not even query preferences
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveOrThrow_WithTenantPreference_ReturnsPreferredProvider()
{
var providerA = new FakeProvider("providerA").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var providerB = new FakeProvider("providerB").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var inner = BuildInnerRegistry(providerA, providerB);
var prefs = new FakePreferenceProvider();
prefs.Set(TenantA, "*", new[] { "providerB" });
var registry = Build(inner, prefs, () => TenantA);
var resolved = registry.ResolveOrThrow(CryptoCapability.Signing, SignatureAlgorithms.Es256);
Assert.Same(providerB, resolved);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveOrThrow_TenantPreferenceProviderEmpty_FallsBackToDefault()
{
var providerA = new FakeProvider("providerA").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var providerB = new FakeProvider("providerB").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var inner = BuildInnerRegistry(providerA, providerB);
var prefs = new FakePreferenceProvider();
// No preferences set for TenantA => empty list
var registry = Build(inner, prefs, () => TenantA);
var resolved = registry.ResolveOrThrow(CryptoCapability.Signing, SignatureAlgorithms.Es256);
// Empty preferences => fallback to inner default => providerA
Assert.Same(providerA, resolved);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveOrThrow_TenantAccessorThrows_FallsBackToDefault()
{
var providerA = new FakeProvider("providerA").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var inner = BuildInnerRegistry(providerA);
var prefs = new FakePreferenceProvider();
var registry = Build(inner, prefs, () => throw new InvalidOperationException("no tenant context"));
var resolved = registry.ResolveOrThrow(CryptoCapability.Signing, SignatureAlgorithms.Es256);
Assert.Same(providerA, resolved);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveOrThrow_PreferredProviderDoesNotSupportAlgorithm_TriesNextThenFallback()
{
var providerA = new FakeProvider("providerA").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var providerB = new FakeProvider("providerB").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Rs256);
var providerC = new FakeProvider("providerC").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var inner = BuildInnerRegistry(providerA, providerB, providerC);
var prefs = new FakePreferenceProvider();
// Tenant prefers providerB first, then providerC; providerB doesn't support Es256
prefs.Set(TenantA, "*", new[] { "providerB", "providerC" });
var registry = Build(inner, prefs, () => TenantA);
var resolved = registry.ResolveOrThrow(CryptoCapability.Signing, SignatureAlgorithms.Es256);
// providerB doesn't support Es256 => providerC does => resolved to providerC
Assert.Same(providerC, resolved);
}
// ---------------------------------------------------------------
// ResolveSigner
// ---------------------------------------------------------------
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveSigner_ExplicitPreferredProvider_OverridesTenantPreference()
{
var providerA = new FakeProvider("providerA").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var providerB = new FakeProvider("providerB").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var inner = BuildInnerRegistry(providerA, providerB);
var prefs = new FakePreferenceProvider();
prefs.Set(TenantA, "*", new[] { "providerB" });
var registry = Build(inner, prefs, () => TenantA);
// Explicit preferredProvider "providerA" should override tenant preference "providerB"
var result = registry.ResolveSigner(
CryptoCapability.Signing,
SignatureAlgorithms.Es256,
new CryptoKeyReference("key-1"),
preferredProvider: "providerA");
Assert.Equal("providerA", result.ProviderName);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveSigner_NoExplicitPreferred_UsesTenantPreference()
{
var providerA = new FakeProvider("providerA").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var providerB = new FakeProvider("providerB").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var inner = BuildInnerRegistry(providerA, providerB);
var prefs = new FakePreferenceProvider();
prefs.Set(TenantA, "*", new[] { "providerB" });
var registry = Build(inner, prefs, () => TenantA);
var result = registry.ResolveSigner(
CryptoCapability.Signing,
SignatureAlgorithms.Es256,
new CryptoKeyReference("key-1"));
Assert.Equal("providerB", result.ProviderName);
}
// ---------------------------------------------------------------
// ResolveHasher
// ---------------------------------------------------------------
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveHasher_NoTenantContext_FallsBackToDefault()
{
var providerA = new FakeProvider("providerA").WithSupport(CryptoCapability.ContentHashing, HashAlgorithms.Sha256);
var providerB = new FakeProvider("providerB").WithSupport(CryptoCapability.ContentHashing, HashAlgorithms.Sha256);
var inner = BuildInnerRegistry(providerA, providerB);
var prefs = new FakePreferenceProvider();
var registry = Build(inner, prefs, () => null);
var result = registry.ResolveHasher(HashAlgorithms.Sha256);
Assert.Equal("providerA", result.ProviderName);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveHasher_WithTenantPreference_ReturnsPreferredProvider()
{
var providerA = new FakeProvider("providerA").WithSupport(CryptoCapability.ContentHashing, HashAlgorithms.Sha256);
var providerB = new FakeProvider("providerB").WithSupport(CryptoCapability.ContentHashing, HashAlgorithms.Sha256);
var inner = BuildInnerRegistry(providerA, providerB);
var prefs = new FakePreferenceProvider();
prefs.Set(TenantA, "*", new[] { "providerB" });
var registry = Build(inner, prefs, () => TenantA);
var result = registry.ResolveHasher(HashAlgorithms.Sha256);
Assert.Equal("providerB", result.ProviderName);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public void ResolveHasher_ExplicitPreferred_OverridesTenantPreference()
{
var providerA = new FakeProvider("providerA").WithSupport(CryptoCapability.ContentHashing, HashAlgorithms.Sha256);
var providerB = new FakeProvider("providerB").WithSupport(CryptoCapability.ContentHashing, HashAlgorithms.Sha256);
var inner = BuildInnerRegistry(providerA, providerB);
var prefs = new FakePreferenceProvider();
prefs.Set(TenantA, "*", new[] { "providerB" });
var registry = Build(inner, prefs, () => TenantA);
var result = registry.ResolveHasher(HashAlgorithms.Sha256, preferredProvider: "providerA");
Assert.Equal("providerA", result.ProviderName);
}
// ---------------------------------------------------------------
// Caching
// ---------------------------------------------------------------
[Trait("Category", TestCategories.Unit)]
[Fact]
public void PreferencesCached_SecondCallDoesNotQueryProvider()
{
var providerA = new FakeProvider("providerA").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var providerB = new FakeProvider("providerB").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var inner = BuildInnerRegistry(providerA, providerB);
var prefs = new FakePreferenceProvider();
prefs.Set(TenantA, "*", new[] { "providerB" });
var registry = Build(inner, prefs, () => TenantA, cacheTtl: TimeSpan.FromMinutes(5));
// First call: cache miss -> hits provider
registry.ResolveOrThrow(CryptoCapability.Signing, SignatureAlgorithms.Es256);
Assert.Equal(1, prefs.CallCount);
// Second call: cache hit -> no additional provider query
registry.ResolveOrThrow(CryptoCapability.Signing, SignatureAlgorithms.Es256);
Assert.Equal(1, prefs.CallCount);
}
// ---------------------------------------------------------------
// Providers property delegates to inner
// ---------------------------------------------------------------
[Trait("Category", TestCategories.Unit)]
[Fact]
public void Providers_DelegatesToInnerRegistry()
{
var providerA = new FakeProvider("providerA").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var inner = BuildInnerRegistry(providerA);
var prefs = new FakePreferenceProvider();
var registry = Build(inner, prefs, () => null);
Assert.Single(registry.Providers);
Assert.Contains(registry.Providers, p => p.Name == "providerA");
}
// ---------------------------------------------------------------
// TryResolve delegates to inner
// ---------------------------------------------------------------
[Trait("Category", TestCategories.Unit)]
[Fact]
public void TryResolve_DelegatesToInnerRegistry()
{
var providerA = new FakeProvider("providerA").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var inner = BuildInnerRegistry(providerA);
var prefs = new FakePreferenceProvider();
var registry = Build(inner, prefs, () => TenantA);
Assert.True(registry.TryResolve("providerA", out var resolved));
Assert.Same(providerA, resolved);
Assert.False(registry.TryResolve("nonexistent", out _));
}
// ---------------------------------------------------------------
// Multi-tenant isolation
// ---------------------------------------------------------------
[Trait("Category", TestCategories.Unit)]
[Fact]
public void DifferentTenants_ResolveDifferentProviders()
{
var providerA = new FakeProvider("providerA").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var providerB = new FakeProvider("providerB").WithSupport(CryptoCapability.Signing, SignatureAlgorithms.Es256);
var inner = BuildInnerRegistry(providerA, providerB);
var prefs = new FakePreferenceProvider();
prefs.Set(TenantA, "*", new[] { "providerA" });
prefs.Set(TenantB, "*", new[] { "providerB" });
string currentTenant = TenantA;
var registry = Build(inner, prefs, () => currentTenant);
// Tenant A resolves providerA
var resolvedA = registry.ResolveOrThrow(CryptoCapability.Signing, SignatureAlgorithms.Es256);
Assert.Same(providerA, resolvedA);
// Switch to Tenant B: resolves providerB
currentTenant = TenantB;
var resolvedB = registry.ResolveOrThrow(CryptoCapability.Signing, SignatureAlgorithms.Es256);
Assert.Same(providerB, resolvedB);
}
}