using Bunit; using Bunit.TestDoubles; using Microsoft.AspNetCore.Components; using Microsoft.Extensions.DependencyInjection; using ZB.MOM.WW.Audit; using ZB.MOM.WW.Secrets.Abstractions; using ZB.MOM.WW.Secrets.Ui.Components; using ZB.MOM.WW.Secrets.Ui.Tests.Fakes; namespace ZB.MOM.WW.Secrets.Ui.Tests; public class SecretEditorTests : TestContext { private const string EnteredValue = "sup3r-s3cret-value!"; private (RecordingSecretStore Store, CapturingAuditWriter Audit, FakeCipher Cipher) RegisterServices() { var store = new RecordingSecretStore(); var audit = new CapturingAuditWriter(); var cipher = new FakeCipher(); Services.AddSingleton(store); Services.AddSingleton(audit); Services.AddSingleton(cipher); return (store, audit, cipher); } [Fact] public void SecretEditor_Add_EncryptsAndUpserts() { var (store, audit, cipher) = RegisterServices(); var auth = this.AddTestAuthorization(); auth.SetAuthorized("carol"); auth.SetPolicies(SecretsAuthorization.ManagePolicy); var cut = RenderComponent(p => p .Add(c => c.OnSaved, () => { })); // The value control is a masked password input. var valueInput = cut.Find("[data-testid=secret-value]"); Assert.Equal("password", valueInput.GetAttribute("type")); cut.Find("[data-testid=secret-name]").Change("app/thing"); cut.Find("[data-testid=secret-description]").Change("A thing"); cut.Find("[data-testid=secret-content-type]").Change(nameof(SecretContentType.Json)); valueInput.Change(EnteredValue); cut.Find("[data-testid=editor-submit]").Click(); // Exactly one upsert, round-tripping to the entered value under the entered name. Assert.Single(store.Upserts); StoredSecret row = store.Upserts[0]; Assert.Equal("app/thing", row.Name.Value); Assert.Equal(SecretContentType.Json, row.ContentType); Assert.Equal("A thing", row.Description); Assert.Equal("carol", row.UpdatedBy); Assert.Equal("carol", row.CreatedBy); Assert.Equal(EnteredValue, cipher.Decrypt(row)); // A single secret.add audit was written, Success, correct target/actor, and NO value leak. Assert.Single(audit.Events); AuditEvent evt = audit.Events[0]; Assert.Equal("secret.add", evt.Action); Assert.Equal(AuditOutcome.Success, evt.Outcome); Assert.Equal("Secrets", evt.Category); Assert.Equal("app/thing", evt.Target); Assert.Equal("carol", evt.Actor); AssertNoValueLeak(evt); } [Fact] public void SecretEditor_Rotate_PreservesName_Overwrites() { var (store, audit, cipher) = RegisterServices(); var name = new SecretName("db/primary"); var auth = this.AddTestAuthorization(); auth.SetAuthorized("dave"); auth.SetPolicies(SecretsAuthorization.ManagePolicy); var cut = RenderComponent(p => p .Add(c => c.ExistingName, name) .Add(c => c.OnSaved, () => { })); // In rotate mode the name field is read-only and shows the existing name. var nameInput = cut.Find("[data-testid=secret-name]"); Assert.True(nameInput.HasAttribute("readonly")); Assert.Equal("db/primary", nameInput.GetAttribute("value")); cut.Find("[data-testid=secret-value]").Change("rotated-value"); cut.Find("[data-testid=editor-submit]").Click(); Assert.Single(store.Upserts); StoredSecret row = store.Upserts[0]; Assert.Equal("db/primary", row.Name.Value); Assert.Equal("rotated-value", cipher.Decrypt(row)); Assert.Single(audit.Events); Assert.Equal("secret.rotate", audit.Events[0].Action); Assert.Equal("db/primary", audit.Events[0].Target); AssertNoValueLeak(audit.Events[0], "rotated-value"); } private static void AssertNoValueLeak(AuditEvent evt, string value = EnteredValue) { foreach (string? field in new[] { evt.Action, evt.Actor, evt.Category, evt.Target, evt.DetailsJson, evt.ToString() }) { if (field is not null) { Assert.DoesNotContain(value, field, StringComparison.Ordinal); } } } }