using System.Security.Claims; using Bunit; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.DependencyInjection; using NSubstitute; using ZB.MOM.WW.ScadaBridge.CentralUI.Auth; using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Health; using ZB.MOM.WW.ScadaBridge.CentralUI.Services; using ZB.MOM.WW.ScadaBridge.DeploymentManager.PendingUpdates; using ZB.MOM.WW.ScadaBridge.Security; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Monitoring; /// Gitea #63: pending-updates tiles on the Health dashboard. public class PendingUpdatesHealthTilesTests : BunitContext { private static SitePendingUpdates Row(int id, int stale, ArtifactDeploymentState a = ArtifactDeploymentState.Current) => new(id, $"S{id}", $"Site {id}", a, DateTimeOffset.UtcNow, 10, stale, 0); private IRenderedComponent RenderFor(IPendingUpdatesCache cache, params Claim[] claims) { var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth")); Services.AddScoped(); Services.AddSingleton(cache); return Render(ps => ps.AddCascadingValue(Task.FromResult(new AuthenticationState(user)))); } private static IPendingUpdatesCache CacheReturning(params SitePendingUpdates[] rows) { var cache = Substitute.For(); cache.GetAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromResult(new PendingUpdatesSnapshot(DateTimeOffset.UtcNow, rows))); return cache; } private static Claim Role(string r) => new(JwtTokenService.RoleClaimType, r); /// A Deployer sees both counts, highlighted when non-zero. [Fact] public void Deployer_SeesBothCounts() { var cut = RenderFor( CacheReturning(Row(1, 4), Row(2, 3, ArtifactDeploymentState.OutOfDate)), Role(Roles.Deployer)); cut.WaitForAssertion(() => { Assert.Contains("7", cut.Find("[data-test='pending-updates-instances']").TextContent); Assert.Contains("1", cut.Find("[data-test='pending-updates-artifact-sites']").TextContent); Assert.Contains("border-warning", cut.Find("[data-test='pending-updates-instances']").ClassName); }); } /// A Viewer sees nothing and triggers no fleet read. [Fact] public void Viewer_SeesNothing_AndNoFleetRead() { var cache = CacheReturning(Row(1, 4)); var cut = RenderFor(cache, Role("Viewer")); Assert.True(string.IsNullOrWhiteSpace(cut.Markup)); cache.DidNotReceive().GetAsync(Arg.Any(), Arg.Any()); } /// A site-scoped Deployer's totals exclude sites outside the scope. [Fact] public void SiteScopedDeployer_CountsOnlyPermittedSites() { var cut = RenderFor( CacheReturning(Row(1, 4), Row(2, 9)), Role(Roles.Deployer), new Claim(JwtTokenService.SiteIdClaimType, "1")); cut.WaitForAssertion(() => Assert.Equal("4", cut.Find("[data-test='pending-updates-instances'] h3").TextContent.Trim())); } /// A failed read is shown as unavailable, never as zero. [Fact] public void ReadFailure_ShowsUnavailable_NotZero() { var cache = Substitute.For(); cache.GetAsync(Arg.Any(), Arg.Any()) .Returns(Task.FromException(new InvalidOperationException("boom"))); var cut = RenderFor(cache, Role(Roles.Administrator)); cut.WaitForAssertion(() => { Assert.Contains("boom", cut.Find("[data-test='pending-updates-error']").TextContent); Assert.Equal("—", cut.Find("[data-test='pending-updates-instances'] h3").TextContent.Trim()); }); } /// An invalidation re-reads the cache. [Fact] public void Invalidated_Rereads() { var cache = Substitute.For(); cache.GetAsync(Arg.Any(), Arg.Any()).Returns( Task.FromResult(new PendingUpdatesSnapshot(DateTimeOffset.UtcNow, [Row(1, 5)])), Task.FromResult(new PendingUpdatesSnapshot(DateTimeOffset.UtcNow, [Row(1, 0)]))); var cut = RenderFor(cache, Role(Roles.Deployer)); cut.WaitForAssertion(() => Assert.Equal("5", cut.Find("[data-test='pending-updates-instances'] h3").TextContent.Trim())); cache.Invalidated += NSubstitute.Raise.Event(); cut.WaitForAssertion(() => Assert.Equal("0", cut.Find("[data-test='pending-updates-instances'] h3").TextContent.Trim())); } }