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.Components; using ZB.MOM.WW.Secrets.Ui.Tests.Fakes; namespace ZB.MOM.WW.Secrets.Ui.Tests; public class ConfirmDeleteModalTests : TestContext { [Fact] public void ConfirmDeleteModal_IsInPageModal_NoJsDialog() { // Strict JS interop mode: any IJSRuntime call (a JS confirm/alert dialog) would throw, // proving the confirmation is a rendered in-page modal, not a browser dialog. JSInterop.Mode = JSRuntimeMode.Strict; var store = new RecordingSecretStore(); var audit = new CapturingAuditWriter(); Services.AddSingleton(store); Services.AddSingleton(audit); var name = new SecretName("db/primary"); var auth = this.AddTestAuthorization(); auth.SetAuthorized("carol"); auth.SetPolicies(SecretsAuthorization.ManagePolicy); bool deleted = false; var cut = RenderComponent(p => p .Add(c => c.Visible, true) .Add(c => c.Name, name) .Add(c => c.OnDeleted, () => deleted = true)); // The confirmation is a rendered in-page modal element. Assert.NotEmpty(cut.FindAll(".zb-secrets-modal")); Assert.Contains("db/primary", cut.Markup); cut.Find("[data-testid=confirm-delete]").Click(); // Confirm tombstoned the row (with the resolved actor) and wrote a secret.delete audit. Assert.Single(store.Deletes); Assert.Equal("db/primary", store.Deletes[0].Name.Value); Assert.Equal("carol", store.Deletes[0].Actor); Assert.True(deleted); Assert.Single(audit.Events); AuditEvent evt = audit.Events[0]; Assert.Equal("secret.delete", evt.Action); Assert.Equal(AuditOutcome.Success, evt.Outcome); Assert.Equal("Secrets", evt.Category); Assert.Equal("db/primary", evt.Target); Assert.Equal("carol", evt.Actor); } [Fact] public void ConfirmDeleteModal_Cancel_DoesNotDelete() { var store = new RecordingSecretStore(); var audit = new CapturingAuditWriter(); Services.AddSingleton(store); Services.AddSingleton(audit); var name = new SecretName("db/primary"); var auth = this.AddTestAuthorization(); auth.SetAuthorized("carol"); auth.SetPolicies(SecretsAuthorization.ManagePolicy); bool cancelled = false; var cut = RenderComponent(p => p .Add(c => c.Visible, true) .Add(c => c.Name, name) .Add(c => c.OnCancelled, () => cancelled = true)); cut.Find("[data-testid=cancel-delete]").Click(); Assert.True(cancelled); Assert.Empty(store.Deletes); Assert.Empty(audit.Events); } [Fact] public void ConfirmDeleteModal_StoreFault_AuditsFailure_ShowsError_DoesNotThrow() { var store = new RecordingSecretStore { DeleteFault = new MasterKeyUnavailableException("KEK unavailable."), }; var audit = new CapturingAuditWriter(); Services.AddSingleton(store); Services.AddSingleton(audit); var name = new SecretName("db/primary"); var auth = this.AddTestAuthorization(); auth.SetAuthorized("carol"); auth.SetPolicies(SecretsAuthorization.ManagePolicy); bool deleted = false; var cut = RenderComponent(p => p .Add(c => c.Visible, true) .Add(c => c.Name, name) .Add(c => c.OnDeleted, () => deleted = true)); // The click must not throw into the Blazor event pipeline. cut.Find("[data-testid=confirm-delete]").Click(); // OnDeleted not raised; a Failure audit recorded; an error surfaced. Assert.False(deleted); Assert.Single(audit.Events); AuditEvent evt = audit.Events[0]; Assert.Equal("secret.delete", evt.Action); Assert.Equal(AuditOutcome.Failure, evt.Outcome); Assert.Equal("Secrets", evt.Category); Assert.Equal("db/primary", evt.Target); Assert.Equal("carol", evt.Actor); Assert.NotEmpty(cut.FindAll("[data-testid=delete-error]")); } [Fact] public void ConfirmDeleteModal_NotVisible_RendersNothing() { var store = new RecordingSecretStore(); var audit = new CapturingAuditWriter(); Services.AddSingleton(store); Services.AddSingleton(audit); var name = new SecretName("db/primary"); this.AddTestAuthorization().SetAuthorized("carol"); var cut = RenderComponent(p => p .Add(c => c.Visible, false) .Add(c => c.Name, name)); Assert.Empty(cut.FindAll(".zb-secrets-modal")); } /// /// scadaproj#2 regression pins. The modal previously used the bare modal class and /// shipped no CSS, silently depending on the host having no opinion about it — Bootstrap 5's /// .modal { display: none } made the overlay permanently invisible on every /// Bootstrap host (ScadaBridge, OtOpcUa, HistorianGateway) while every handler kept working. /// [Fact] public void ConfirmDeleteModal_UsesNoFrameworkReservedClassNames() { var cut = RenderVisibleModal(); // Bootstrap owns these selectors; any element carrying one is again at the mercy of // whatever stylesheet the host happens to link. string[] reserved = ["modal", "modal-title", "modal-backdrop", "modal-dialog", "modal-content"]; foreach (string cls in reserved) { Assert.Empty(cut.FindAll($".{cls}")); } // The elements themselves still render, under the component-scoped vocabulary. Assert.NotEmpty(cut.FindAll(".zb-secrets-modal")); Assert.NotEmpty(cut.FindAll(".zb-secrets-modal-backdrop")); Assert.NotEmpty(cut.FindAll(".zb-secrets-modal-card")); } [Fact] public void ConfirmDeleteModal_ShipsItsOwnStyles() { var cut = RenderVisibleModal(); // The RCL must carry the modal's CSS itself: no family host links a Secrets.Ui // stylesheet, and three of the four never link a scoped-CSS bundle at all — so any // styling delivered outside the component silently fails to load exactly like the // original defect. The inline block must both position the overlay and give it a // display mode, or a host reset/framework rule can hide it again. var style = cut.FindAll("style").SingleOrDefault(); Assert.NotNull(style); Assert.Contains(".zb-secrets-modal", style.TextContent); Assert.Contains("position: fixed", style.TextContent); Assert.Contains("display: flex", style.TextContent); } private IRenderedComponent RenderVisibleModal() { Services.AddSingleton(new RecordingSecretStore()); Services.AddSingleton(new CapturingAuditWriter()); this.AddTestAuthorization().SetAuthorized("carol"); return RenderComponent(p => p .Add(c => c.Visible, true) .Add(c => c.Name, new SecretName("db/primary"))); } }