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."); + }); + } +}