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