using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using StellaOps.Authority.Plugins.Abstractions; using StellaOps.Authority.Plugin.Standard.Storage; using StellaOps.Authority.Storage.Mongo.Documents; using StellaOps.Authority.Storage.Mongo.Stores; using Xunit; namespace StellaOps.Authority.Plugin.Standard.Tests; public class StandardClientProvisioningStoreTests { [Fact] public async Task CreateOrUpdateAsync_HashesSecretAndPersistsDocument() { var store = new TrackingClientStore(); var revocations = new TrackingRevocationStore(); var provisioning = new StandardClientProvisioningStore("standard", store, revocations, TimeProvider.System); var registration = new AuthorityClientRegistration( clientId: "bootstrap-client", confidential: true, displayName: "Bootstrap", clientSecret: "SuperSecret1!", allowedGrantTypes: new[] { "client_credentials" }, allowedScopes: new[] { "scopeA" }); var result = await provisioning.CreateOrUpdateAsync(registration, CancellationToken.None); Assert.True(result.Succeeded); Assert.True(store.Documents.TryGetValue("bootstrap-client", out var document)); Assert.NotNull(document); Assert.Equal(AuthoritySecretHasher.ComputeHash("SuperSecret1!"), document!.SecretHash); Assert.Equal("standard", document.Plugin); var descriptor = await provisioning.FindByClientIdAsync("bootstrap-client", CancellationToken.None); Assert.NotNull(descriptor); Assert.Equal("bootstrap-client", descriptor!.ClientId); Assert.True(descriptor.Confidential); Assert.Contains("client_credentials", descriptor.AllowedGrantTypes); Assert.Contains("scopeA", descriptor.AllowedScopes); } private sealed class TrackingClientStore : IAuthorityClientStore { public Dictionary Documents { get; } = new(StringComparer.OrdinalIgnoreCase); public ValueTask FindByClientIdAsync(string clientId, CancellationToken cancellationToken) { Documents.TryGetValue(clientId, out var document); return ValueTask.FromResult(document); } public ValueTask UpsertAsync(AuthorityClientDocument document, CancellationToken cancellationToken) { Documents[document.ClientId] = document; return ValueTask.CompletedTask; } public ValueTask DeleteByClientIdAsync(string clientId, CancellationToken cancellationToken) { var removed = Documents.Remove(clientId); return ValueTask.FromResult(removed); } } private sealed class TrackingRevocationStore : IAuthorityRevocationStore { public List Upserts { get; } = new(); public ValueTask UpsertAsync(AuthorityRevocationDocument document, CancellationToken cancellationToken) { Upserts.Add(document); return ValueTask.CompletedTask; } public ValueTask RemoveAsync(string category, string revocationId, CancellationToken cancellationToken) => ValueTask.FromResult(true); public ValueTask> GetActiveAsync(DateTimeOffset asOf, CancellationToken cancellationToken) => ValueTask.FromResult>(Array.Empty()); } }