From 12b86bea7aca2e13a0bd13f8262274c0efff02d3 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 20 May 2026 19:49:11 -0400 Subject: [PATCH] feat(ui): scaffold Audit Log page + Audit nav group (#23 M7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the central-side Audit Log page scaffold at /audit/log (M7-T1) and introduces a new Audit nav group (M7-T9) that hosts both the new page and the renamed Configuration Audit Log. The page body is intentionally a heading + two placeholders — Bundle B will land the AuditFilterBar and AuditResultsGrid behind them. The Audit nav group sits between Monitoring and the per-user footer; both items inside are Admin-only, so the section header lives inside the RequireAdmin AuthorizeView (non-admins see no orphan header). bUnit scaffold tests pin the page heading, the section header order, and the two child links; the existing 338 CentralUI tests continue to pass. --- .../Components/Layout/NavMenu.razor | 9 +- .../Components/Pages/Audit/AuditLogPage.razor | 16 +++ .../Pages/Audit/AuditLogPage.razor.cs | 12 ++ .../Pages/AuditLogPageScaffoldTests.cs | 108 ++++++++++++++++++ 4 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor create mode 100644 src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor.cs create mode 100644 tests/ScadaLink.CentralUI.Tests/Pages/AuditLogPageScaffoldTests.cs diff --git a/src/ScadaLink.CentralUI/Components/Layout/NavMenu.razor b/src/ScadaLink.CentralUI/Components/Layout/NavMenu.razor index 851c4f7..798d306 100644 --- a/src/ScadaLink.CentralUI/Components/Layout/NavMenu.razor +++ b/src/ScadaLink.CentralUI/Components/Layout/NavMenu.razor @@ -108,9 +108,16 @@ - @* Configuration Audit Log — Admin only *@ + @* Audit — Admin only. Hosts the new Audit Log page (#23 M7) + and the renamed Configuration Audit Log (IAuditService + config-change viewer). Both items are Admin-gated, so + the section header sits inside the same policy block. *@ + + diff --git a/src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor b/src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor new file mode 100644 index 0000000..8d250e7 --- /dev/null +++ b/src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor @@ -0,0 +1,16 @@ +@page "/audit/log" +@attribute [Authorize] + +Audit Log + +
+

Audit Log

+ + @* AuditFilterBar will go here (Bundle B). *@ +
+
+ + @* AuditResultsGrid will go here (Bundle B). *@ +
+
+
diff --git a/src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor.cs b/src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor.cs new file mode 100644 index 0000000..311f369 --- /dev/null +++ b/src/ScadaLink.CentralUI/Components/Pages/Audit/AuditLogPage.razor.cs @@ -0,0 +1,12 @@ +namespace ScadaLink.CentralUI.Components.Pages.Audit; + +/// +/// Code-behind for the central Audit Log page (#23 M7-T1). The Bundle A +/// scaffold has no behaviour — the filter bar and results grid arrive in +/// Bundle B (M7-T2..M7-T7). Keeping the partial class in place now lets +/// later bundles add injected services and event handlers without +/// touching the route or page-title markup. +/// +public partial class AuditLogPage +{ +} diff --git a/tests/ScadaLink.CentralUI.Tests/Pages/AuditLogPageScaffoldTests.cs b/tests/ScadaLink.CentralUI.Tests/Pages/AuditLogPageScaffoldTests.cs new file mode 100644 index 0000000..3ab4892 --- /dev/null +++ b/tests/ScadaLink.CentralUI.Tests/Pages/AuditLogPageScaffoldTests.cs @@ -0,0 +1,108 @@ +using System.Security.Claims; +using Bunit; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.DependencyInjection; +using ScadaLink.Security; +using AuditLogPage = ScadaLink.CentralUI.Components.Pages.Audit.AuditLogPage; +using NavMenu = ScadaLink.CentralUI.Components.Layout.NavMenu; + +namespace ScadaLink.CentralUI.Tests.Pages; + +/// +/// Scaffold tests for the new Audit Log page (#23 M7-T1) and the Audit +/// nav group that hosts both it and the renamed Configuration Audit Log +/// (#23 M7 Bundle A). +/// +/// These are render-only smoke tests — the filter bar and results grid +/// are intentional placeholders that Bundle B fills in. The tests pin +/// the page route, page heading, nav group label, and the two child +/// links so later bundles cannot regress the scaffolding. +/// +public class AuditLogPageScaffoldTests : BunitContext +{ + private static ClaimsPrincipal BuildPrincipal(params string[] roles) + { + var claims = new List { new("Username", "tester") }; + claims.AddRange(roles.Select(r => new Claim(JwtTokenService.RoleClaimType, r))); + return new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth")); + } + + private IRenderedComponent RenderAuditLogPage(params string[] roles) + { + var user = BuildPrincipal(roles); + Services.AddSingleton(new TestAuthStateProvider(user)); + Services.AddAuthorizationCore(); + AuthorizationPolicies.AddScadaLinkAuthorization(Services); + Services.AddSingleton(); + return Render(); + } + + private IRenderedComponent RenderNavMenu(params string[] roles) + { + var user = BuildPrincipal(roles); + Services.AddSingleton(new TestAuthStateProvider(user)); + Services.AddAuthorizationCore(); + AuthorizationPolicies.AddScadaLinkAuthorization(Services); + Services.AddSingleton(); + + var host = Render(parameters => parameters + .Add(p => p.ChildContent, (RenderFragment)(builder => + { + builder.OpenComponent(0); + builder.CloseComponent(); + }))); + + return host.FindComponent(); + } + + [Fact] + public void AuditLogPage_Renders_PageHeading() + { + var cut = RenderAuditLogPage("Admin"); + + cut.WaitForAssertion(() => + { + // The H1 is the only positive scaffold assertion — the filter + // bar and grid are still placeholders the Bundle B work fills. + Assert.Contains(" + { + Assert.Contains(">Audit<", cut.Markup); + Assert.Contains("/audit/log", cut.Markup); + }); + } + + [Fact] + public void NavMenu_Contains_ConfigurationAuditLog_Link_UnderAuditGroup() + { + var cut = RenderNavMenu("Admin", "Design", "Deployment"); + + cut.WaitForAssertion(() => + { + // Both audit pages must appear after the Audit section header + // in the rendered nav. We check both links + that the header + // comes before either link in the markup, so they are in the + // Audit group rather than orphaned under Monitoring. + Assert.Contains("/audit/configuration", cut.Markup); + Assert.Contains("/audit/log", cut.Markup); + var headerIdx = cut.Markup.IndexOf(">Audit<", StringComparison.Ordinal); + var configIdx = cut.Markup.IndexOf("/audit/configuration", StringComparison.Ordinal); + var logIdx = cut.Markup.IndexOf("/audit/log", StringComparison.Ordinal); + Assert.True(headerIdx >= 0 && headerIdx < configIdx, + "Audit section header must precede the Configuration Audit Log link."); + Assert.True(headerIdx >= 0 && headerIdx < logIdx, + "Audit section header must precede the Audit Log link."); + }); + } +}