using System.Reflection;
using Microsoft.AspNetCore.Authorization;
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 side-rail link to /admin/secrets and its role gate. The page has worked since
/// lmxopcua#483, but reachability is a separate property from correctness: before this, the only
/// way to the secrets UI was typing the URL, which is the same orphaned-surface shape as Gitea
/// #546 () — everything builds, tests and deploys while no
/// user can get there.
///
///
/// Source scan rather than a rendering assertion: the repo has no bUnit (the central
/// PackageVersion for it is referenced by no project), and razor layout markup — unlike
/// a page's @page/@attribute directives — does not survive into class metadata
/// any form a reflection guard can read. So this proves the link and its gate are *written*;
/// that the gate actually admits an Administrator and hides the item from a Designer is
/// live-verified on the docker-dev rig, per this repo's Razor convention.
///
[Trait("Category", "Unit")]
public sealed class SecretsNavLinkTests
{
private const string NavItem = "The reachability pin: without this the secrets UI is URL-only.
[Fact]
public void The_rail_links_to_the_secrets_page()
{
ReadMainLayoutSource().ShouldContain(
NavItem,
Case.Sensitive,
customMessage:
"the side rail no longer links to /admin/secrets — the secrets UI is reachable " +
"only by typing the URL, which is how it shipped before this test existed");
}
///
/// The link must sit INSIDE the AuthorizeView, not merely somewhere in the same file — an
/// unnested pair would leave the item visible to every authenticated user while reading, to
/// a scan for both strings, exactly like a gated one. Ordering is the strongest structural
/// claim a text scan can make; whether the gate actually hides the item is live-verified.
///
[Fact]
public void The_secrets_link_is_nested_inside_the_authorize_view()
{
var source = ReadMainLayoutSource();
var open = source.IndexOf("", open + 1, StringComparison.Ordinal);
open.ShouldBeGreaterThanOrEqualTo(0, "the rail has no policy-gated AuthorizeView at all");
close.ShouldBeGreaterThanOrEqualTo(0, "the gating AuthorizeView is never closed");
item.ShouldBeInRange(open, close,
"the /admin/secrets rail item is outside the AuthorizeView — it is visible to every " +
"authenticated user, and a scan that only looked for both tags would not notice");
}
///
/// The gate must be expressed as the page's own policy constant. Roles="Administrator"
/// or a "secrets:manage" literal would render identically today and then silently drift the
/// moment the RCL changed its policy — the rail would advertise a page that rejects the user,
/// or hide one that would have admitted them, with nothing failing.
///
[Fact]
public void The_gate_is_the_pages_own_policy_constant_not_a_role_or_a_literal()
{
var source = ReadMainLayoutSource();
source.ShouldContain("SecretsAuthorization.ManagePolicy", Case.Sensitive);
source.ShouldNotContain($"\"{SecretsAuthorization.ManagePolicy}\"", Case.Sensitive,
customMessage: "the rail hard-codes the policy string instead of referencing the constant");
source.ShouldNotContain("
/// Cross-check that the constant the rail references is the one the page actually enforces.
/// The two assertions above are satisfiable by a rail gated on a policy no page uses; this is
/// what makes them mean "the rail shows exactly who the page will admit".
///
[Fact]
public void The_rails_policy_is_the_one_the_page_enforces()
{
var page = typeof(SecretsAdmin).GetCustomAttributes(inherit: false).Single();
page.Policy.ShouldBe(SecretsAuthorization.ManagePolicy,
"the rail gates on SecretsAuthorization.ManagePolicy, so the page must enforce it");
}
}