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()));
}
}