using System.Reflection; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Web; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages; using ZB.MOM.WW.Secrets.Ui; namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests; /// /// Pins the host-side wiring that makes /admin/secrets interactive (lmxopcua#483). /// This host's router is static with per-page @rendermode opt-in; routing straight to /// the Secrets.Ui RCL's routable page rendered it with no circuit — the page displayed but /// every @onclick was dead, and nothing failed. The fix is a wrapper page in this /// assembly that owns the route and carries the render mode. Razor directives compile to /// class-level attributes, so a metadata scan is authoritative (same technique as /// PageAuthorizationGuardTests; the repo has no bUnit). /// public class SecretsPageWiringTests { /// /// THE #483 regression pin: without a render mode this host serves the page as static SSR /// and it silently loses all interactivity. /// [Fact] public void Wrapper_declares_the_InteractiveServer_render_mode() { var mode = typeof(SecretsAdmin).GetCustomAttributes(inherit: false) .SingleOrDefault(); mode.ShouldNotBeNull( "/admin/secrets lost its @rendermode — in this host's static router that renders a " + "dead page (displays, but no circuit and every @onclick inert), exactly lmxopcua#483"); mode.Mode.ShouldBeOfType(); } /// /// The wrapper must shadow the RCL page's route exactly — if Secrets.Ui ever moves its /// page, the wrapper must move with it or the nav rail and bookmarks split from the RCL. /// [Fact] public void Wrapper_owns_the_same_route_as_the_RCL_page() { string[] wrapper = [.. typeof(SecretsAdmin).GetCustomAttributes(inherit: false).Select(r => r.Template)]; string[] rcl = [.. typeof(SecretsPage).GetCustomAttributes(inherit: false).Select(r => r.Template)]; wrapper.ShouldBe(["/admin/secrets"]); wrapper.ShouldBe(rcl, "the wrapper must track the RCL page's route exactly"); } /// /// The router enforces [Authorize] only on the routed component — which is now the wrapper, /// with the RCL page rendered as a plain child whose own attribute is inert. The wrapper /// must therefore re-state the RCL page's policy verbatim, or the fix silently widens access. /// [Fact] public void Wrapper_restates_the_RCL_pages_authorization_policy() { var wrapper = typeof(SecretsAdmin).GetCustomAttributes(inherit: false).Single(); var rcl = typeof(SecretsPage).GetCustomAttributes(inherit: false).Single(); wrapper.Policy.ShouldBe(SecretsAuthorization.ManagePolicy); wrapper.Policy.ShouldBe(rcl.Policy, "the wrapper must enforce exactly what the RCL page declares"); } }