using System.Text.Json; using Akka.Actor; using Akka.TestKit.Xunit2; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using NSubstitute; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit; namespace ZB.MOM.WW.ScadaBridge.ManagementService.Tests; /// /// The VerifyAuditChainHeads management command (ScadaBridge#107 item 1): role gate, /// site-scope refusal, input bounds, and the verdicts reaching the wire with enums as names. /// public class VerifyAuditChainHeadsCommandTests : TestKit { private readonly IAuditChainHeadStore _store = Substitute.For(); private static readonly AuditChainHeadRecord Record = new( 1, "2026-05", 12, new string('a', 64), null, null, 12, 0, new DateTime(2026, 5, 3, 0, 0, 0, DateTimeKind.Utc), new DateTime(2026, 5, 3, 1, 0, 0, DateTimeKind.Utc), "central-a"); private IActorRef CreateActor() { var services = new ServiceCollection().AddTestConfigurationTransaction(); services.AddScoped(_ => _store); return Sys.ActorOf(Props.Create(() => new ManagementActor( services.BuildServiceProvider(), NullLogger.Instance))); } private static ManagementEnvelope Envelope(object command, string[] sites, params string[] roles) => new(new AuthenticatedUser("u", "U", roles, sites), command, Guid.NewGuid().ToString("N")); /// A Viewer gets the store's verdicts; the records reach the store unchanged. [Fact] public void Viewer_GetsTheVerdicts_ForTheSuppliedRecords() { var check = new AuditChainHeadCheck("2026-05", 12, Record.ExportedAtUtc, "central-a", AuditChainHeadStatus.Mismatch, 12, "rewritten"); _store.VerifyExportedHeadsAsync(Arg.Any>(), Arg.Any()) .Returns(new AuditChainHeadVerificationResult(false, 1, [check])); CreateActor().Tell(Envelope(new VerifyAuditChainHeadsCommand([Record]), [], "Viewer")); var response = ExpectMsg(TimeSpan.FromSeconds(5)); using var json = JsonDocument.Parse(response.JsonData); Assert.False(json.RootElement.GetProperty("verified").GetBoolean()); Assert.Equal("Mismatch", json.RootElement.GetProperty("checks")[0].GetProperty("status").GetString()); _store.Received(1).VerifyExportedHeadsAsync( Arg.Is>(r => r.Count == 1 && r[0] == Record), Arg.Any()); } /// A Designer/Deployer without an OperationalAudit role is refused. [Fact] public void NonAuditRole_IsUnauthorized() { CreateActor().Tell(Envelope(new VerifyAuditChainHeadsCommand([Record]), [], "Designer", "Deployer")); ExpectMsg(TimeSpan.FromSeconds(5)); _store.DidNotReceiveWithAnyArgs().VerifyExportedHeadsAsync(default!, default); } /// A site-scoped Viewer is refused: the chain spans every site. [Fact] public void SiteScopedViewer_IsRefused() { CreateActor().Tell(Envelope(new VerifyAuditChainHeadsCommand([Record]), ["3"], "Viewer")); var response = ExpectMsg(TimeSpan.FromSeconds(5)); Assert.Contains("spans every site", response.Message); _store.DidNotReceiveWithAnyArgs().VerifyExportedHeadsAsync(default!, default); } /// No records, or more than the cap, is a caller error. /// Records supplied. [Theory] [InlineData(0)] [InlineData(ManagementActor.MaxChainHeadRecords + 1)] public void EmptyOrOversizedInput_IsInvalidArgument(int count) { CreateActor().Tell(Envelope( new VerifyAuditChainHeadsCommand(Enumerable.Repeat(Record, count).ToList()), [], "Administrator")); var response = ExpectMsg(TimeSpan.FromSeconds(5)); Assert.Equal("INVALID_ARGUMENT", response.ErrorCode); _store.DidNotReceiveWithAnyArgs().VerifyExportedHeadsAsync(default!, default); } /// A chain lock that cannot be had surfaces as CHAIN_BUSY. [Fact] public void BusyChain_IsChainBusy() { _store.VerifyExportedHeadsAsync(Arg.Any>(), Arg.Any()) .Returns>(_ => throw new TimeoutException("busy")); CreateActor().Tell(Envelope(new VerifyAuditChainHeadsCommand([Record]), [], "Administrator")); Assert.Equal("CHAIN_BUSY", ExpectMsg(TimeSpan.FromSeconds(5)).ErrorCode); } /// Runs under the long-running Ask timeout. [Fact] public void VerifyAuditChainHeads_UsesLongRunningAskTimeout() { Assert.Equal(TimeSpan.FromMinutes(5), ManagementEndpoints.ResolveAskTimeout(new ManagementServiceOptions(), typeof(VerifyAuditChainHeadsCommand))); } }