using System.Collections.Concurrent; using System.Security.Claims; using Akka.Actor; using Bunit; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using NSubstitute; using ZB.MOM.WW.ScadaBridge.CentralUI.Auth; using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Monitoring; using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Messages.RemoteQuery; using ZB.MOM.WW.ScadaBridge.Communication; using ZB.MOM.WW.ScadaBridge.Security; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Auth; public class ParkedMessagesMutationRegressionTests : BunitContext { /// Verifies that a retry for a site with a non-numeric identifier is relayed only while the user's current site grant allows it. /// true to revoke the user's site grant before retrying, false to keep it. /// A task that represents the asynchronous operation. [Theory] [InlineData(false)] [InlineData(true)] public async Task NonnumericSiteIdentifier_RelaysOnlyWhileCurrentSiteGrantAllows(bool revoke) { using var system = ActorSystem.Create("parked-mutation-" + Guid.NewGuid().ToString("N")); var dispatched = new ConcurrentQueue(); var comms = new CommunicationService(Options.Create(new CommunicationOptions()), NullLogger.Instance); comms.SetCommunicationActor(system.ActorOf(Props.Create(() => new ReplyActor(dispatched)))); Services.AddSingleton(comms); var sites = Substitute.For(); sites.GetAllSitesAsync(Arg.Any()).Returns(new List { new("Plant A", "plant-a") { Id = 42 } }); Services.AddSingleton(sites); var auth = new MutableAuth("42"); Services.AddSingleton(auth); Services.AddScoped(); Services.AddAuthorizationCore(); Services.AddLogging(); Services.AddSingleton(Substitute.For()); JSInterop.Mode = JSRuntimeMode.Loose; var page = Render(); await page.Find("select").ChangeAsync(new() { Value = "plant-a" }); page.WaitForAssertion(() => Assert.Contains("ERP", page.Markup)); await page.FindAll("tr").Single(row => row.TextContent.Contains("ERP")).ClickAsync(new()); page.WaitForAssertion(() => Assert.Contains("Retry", page.Markup)); if (revoke) auth.SiteId = "99"; // The same rendered page still contains the old site and row. The click must // resolve SiteIdentifier through Site.Id and consult the live grant. await page.FindAll("button").Single(button => button.TextContent.Trim() == "Retry").ClickAsync(new()); if (revoke) { Assert.Empty(dispatched); Assert.Contains("Site access was revoked", page.Markup); } else { var envelope = Assert.Single(dispatched); Assert.Equal("plant-a", envelope.SiteId); var retry = Assert.IsType(envelope.Message); Assert.Equal("parked-message-1", retry.MessageId); Assert.Equal("plant-a", retry.SiteId); } await system.Terminate(); } private sealed class MutableAuth(string siteId) : AuthenticationStateProvider { /// Gets or sets the site identifier carried in the user's site-scope claim. public string SiteId { get; set; } = siteId; /// public override Task GetAuthenticationStateAsync() => Task.FromResult(new AuthenticationState( new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(JwtTokenService.UsernameClaimType, "operator"), new Claim(JwtTokenService.RoleClaimType, Roles.Deployer), new Claim(JwtTokenService.SiteIdClaimType, SiteId) }, "test")))); } private sealed class ReplyActor : ReceiveActor { /// Initializes the actor with the queue that records relayed retry requests. /// The queue that receives each relayed retry envelope. public ReplyActor(ConcurrentQueue dispatched) { Receive(envelope => { if (envelope.Message is ParkedMessageQueryRequest query) Sender.Tell(new ParkedMessageQueryResponse(query.CorrelationId, envelope.SiteId, new[] { new ParkedMessageEntry("parked-message-1", "ERP", "Send", "Offline", 2, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow) }, 1, 1, 25, true, null, DateTimeOffset.UtcNow)); else if (envelope.Message is ParkedMessageRetryRequest retry) { dispatched.Enqueue(envelope); Sender.Tell(new ParkedMessageRetryResponse(retry.CorrelationId, true)); } }); } } }