using Bunit; using Bunit.TestDoubles; using Microsoft.Extensions.DependencyInjection; using ZB.MOM.WW.Audit; using ZB.MOM.WW.Secrets.Abstractions; using ZB.MOM.WW.Secrets.Ui; using ZB.MOM.WW.Secrets.Ui.Tests.Fakes; namespace ZB.MOM.WW.Secrets.Ui.Tests; public class SecretsListTests : TestContext { private static readonly SecretMetadata SecretOne = new() { Name = new SecretName("db/primary"), Description = "Primary database connection", ContentType = SecretContentType.ConnectionString, KekId = "kek-a", Revision = 3, CreatedUtc = new DateTimeOffset(2026, 7, 1, 0, 0, 0, TimeSpan.Zero), UpdatedUtc = new DateTimeOffset(2026, 7, 10, 12, 0, 0, TimeSpan.Zero), UpdatedBy = "alice", }; private static readonly SecretMetadata SecretTwo = new() { Name = new SecretName("api/token"), Description = "External API token", ContentType = SecretContentType.Text, KekId = "kek-b", Revision = 1, CreatedUtc = new DateTimeOffset(2026, 7, 2, 0, 0, 0, TimeSpan.Zero), UpdatedUtc = new DateTimeOffset(2026, 7, 11, 8, 30, 0, TimeSpan.Zero), UpdatedBy = "bob", }; private void RegisterStore(params SecretMetadata[] rows) { // SecretsList now embeds RevealButton / ConfirmDeleteModal / SecretEditor, which inject the // cipher and audit writer, so those seams must be registered even for list-only assertions. Services.AddSingleton(new FakeSecretStore(rows)); Services.AddSingleton(new FakeCipher()); Services.AddSingleton(new CapturingAuditWriter()); } [Fact] public void Renders_Metadata_ForEachSecret() { RegisterStore(SecretOne, SecretTwo); var auth = this.AddTestAuthorization(); auth.SetAuthorized("alice"); auth.SetPolicies(SecretsAuthorization.ManagePolicy); var cut = RenderComponent(); var markup = cut.Markup; Assert.Contains("db/primary", markup); Assert.Contains("Primary database connection", markup); Assert.Contains(nameof(SecretContentType.ConnectionString), markup); Assert.Contains("api/token", markup); Assert.Contains("External API token", markup); Assert.Contains(nameof(SecretContentType.Text), markup); } [Fact] public void RevealButton_Hidden_WithoutRevealPolicy() { RegisterStore(SecretOne, SecretTwo); var auth = this.AddTestAuthorization(); auth.SetAuthorized("alice"); auth.SetPolicies(SecretsAuthorization.ManagePolicy); var cut = RenderComponent(); Assert.Empty(cut.FindAll("button.reveal-secret")); Assert.DoesNotContain("Reveal", cut.Markup); } [Fact] public void RevealButton_Shown_WithRevealPolicy() { RegisterStore(SecretOne, SecretTwo); var auth = this.AddTestAuthorization(); auth.SetAuthorized("carol"); auth.SetPolicies(SecretsAuthorization.ManagePolicy, SecretsAuthorization.RevealPolicy); var cut = RenderComponent(); // One reveal affordance per secret row. Assert.Equal(2, cut.FindAll("button.reveal-secret").Count); } [Fact] public void EmptyList_ShowsEmptyState() { RegisterStore(); var auth = this.AddTestAuthorization(); auth.SetAuthorized("alice"); auth.SetPolicies(SecretsAuthorization.ManagePolicy); var cut = RenderComponent(); Assert.Contains("No secrets", cut.Markup); } /// Minimal hand-written whose only real behavior is . private sealed class FakeSecretStore(IReadOnlyList rows) : ISecretStore { public Task> ListAsync(bool includeDeleted, CancellationToken ct) => Task.FromResult(rows); public Task GetAsync(SecretName name, CancellationToken ct) => throw new NotSupportedException(); public Task UpsertAsync(StoredSecret row, CancellationToken ct) => throw new NotSupportedException(); public Task DeleteAsync(SecretName name, string? actor, CancellationToken ct) => throw new NotSupportedException(); public Task> GetManifestAsync(CancellationToken ct) => throw new NotSupportedException(); public Task ApplyReplicatedAsync(StoredSecret row, CancellationToken ct) => throw new NotSupportedException(); public Task ApplyRewrapAsync( StoredSecret rewrappedRow, byte[] expectedCurrentWrappedDek, CancellationToken ct) => throw new NotSupportedException(); } }