From f12573d3fa55b7f2b352f6e08d0353781839b55f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 22 Oct 2026 17:57:54 -0400 Subject: [PATCH] fix(adminui): register the /dev/context-menu-demo route only in DEBUG builds (R04 S7) The dev-only demo page shipped routable in the production assembly. Its route moves from @page to a code-behind [Route] under #if DEBUG, so a Release build keeps the component type (nothing referencing it breaks) but has no /dev/* route. The page-authorization guard classifies it under #if DEBUG, and DevRouteGatingTests asserts each configuration's half (Debug: exactly the demo route; Release: none) with a positive control. Both configurations pass. Refs #745 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT --- .../Pages/Dev/ContextMenuDemo.razor | 3 +- .../Pages/Dev/ContextMenuDemo.razor.cs | 16 +++++++ .../Authorization/DevRouteGatingTests.cs | 43 +++++++++++++++++++ .../PageAuthorizationGuardTests.cs | 3 ++ 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Dev/ContextMenuDemo.razor.cs create mode 100644 tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Authorization/DevRouteGatingTests.cs diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Dev/ContextMenuDemo.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Dev/ContextMenuDemo.razor index d4180e38..177e358c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Dev/ContextMenuDemo.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Dev/ContextMenuDemo.razor @@ -1,4 +1,5 @@ -@page "/dev/context-menu-demo" +@* The /dev/context-menu-demo route lives in ContextMenuDemo.razor.cs behind #if DEBUG (R04 S7), so a + Release build ships the component but no route to it. *@ @* Dev-only host page to live-verify the reusable ContextMenu component. *@ @attribute [Authorize(Policy = AdminUiPolicies.AuthenticatedRead)] @rendermode InteractiveServer diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Dev/ContextMenuDemo.razor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Dev/ContextMenuDemo.razor.cs new file mode 100644 index 00000000..8e5c60ec --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Dev/ContextMenuDemo.razor.cs @@ -0,0 +1,16 @@ +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Dev; + +/// +/// Dev-only host page that live-verifies the reusable ContextMenu component. The route is +/// compiled only into DEBUG builds (R04 S7): a Release (production) build keeps the component type — +/// so nothing that references it breaks — but registers no /dev/* route, and the router never +/// serves it. +/// +#if DEBUG +[Microsoft.AspNetCore.Components.Route(RoutePath)] +#endif +public partial class ContextMenuDemo +{ + /// The DEBUG-only route. + public const string RoutePath = "/dev/context-menu-demo"; +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Authorization/DevRouteGatingTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Authorization/DevRouteGatingTests.cs new file mode 100644 index 00000000..4eed93b6 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Authorization/DevRouteGatingTests.cs @@ -0,0 +1,43 @@ +using System.Reflection; +using Microsoft.AspNetCore.Components; +using Shouldly; +using Xunit; +using ZB.MOM.WW.OtOpcUa.AdminUI.Components; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Authorization; + +/// +/// R04 S7: /dev/* routes exist only in DEBUG builds. The test project builds in the same +/// configuration as the AdminUI assembly, so each configuration asserts its own half — run +/// dotnet test -c Release to exercise the production half. +/// +[Trait("Category", "Unit")] +public sealed class DevRouteGatingTests +{ + private static IReadOnlyList DevRoutes() => + typeof(Routes).Assembly.GetTypes() + .SelectMany(t => t.GetCustomAttributes(inherit: false)) + .Select(r => r.Template) + .Where(t => t.StartsWith("/dev/", StringComparison.OrdinalIgnoreCase)) + .ToList(); + + [Fact] + public void Dev_routes_follow_the_build_configuration() + { +#if DEBUG + DevRoutes().ShouldBe([ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Dev.ContextMenuDemo.RoutePath]); +#else + DevRoutes().ShouldBeEmpty(); +#endif + } + + [Fact] + public void Production_routes_are_present_in_every_configuration() + { + // Positive control: the scan sees routes at all (an empty assembly scan would pass the Release half). + typeof(Routes).Assembly.GetTypes() + .SelectMany(t => t.GetCustomAttributes(inherit: false)) + .Select(r => r.Template) + .ShouldContain("/hosts"); + } +} diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Authorization/PageAuthorizationGuardTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Authorization/PageAuthorizationGuardTests.cs index 231c61cc..60ff5323 100644 --- a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Authorization/PageAuthorizationGuardTests.cs +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Authorization/PageAuthorizationGuardTests.cs @@ -59,7 +59,10 @@ public class PageAuthorizationGuardTests // ── AuthenticatedRead (17): dashboards, lists, live tails (read-only) + the dev ContextMenu demo ── [typeof(Home)] = AdminUiPolicies.AuthenticatedRead, +#if DEBUG + // Routable only in DEBUG builds (R04 S7) — a Release build registers no /dev/* route. [typeof(global::ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Dev.ContextMenuDemo)] = AdminUiPolicies.AuthenticatedRead, +#endif [typeof(Fleet)] = AdminUiPolicies.AuthenticatedRead, [typeof(global::ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.FleetDiagnostics)] = AdminUiPolicies.AuthenticatedRead, [typeof(global::ZB.MOM.WW.OtOpcUa.AdminUI.Components.Pages.Hosts)] = AdminUiPolicies.AuthenticatedRead,